first commit

This commit is contained in:
Ale
2025-05-13 12:01:17 +02:00
commit d7881a4461
4852 changed files with 537159 additions and 0 deletions

7
node_modules/pdf-lib/src/api/Embeddable.ts generated vendored Normal file
View File

@@ -0,0 +1,7 @@
/**
* A PDF entity, like images or fonts, which needs to be embedded into the
* document before saving.
*/
export default interface Embeddable {
embed: () => Promise<void>;
}

1395
node_modules/pdf-lib/src/api/PDFDocument.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

44
node_modules/pdf-lib/src/api/PDFDocumentOptions.ts generated vendored Normal file
View File

@@ -0,0 +1,44 @@
import { EmbeddedFileOptions } from 'src/core/embedders/FileEmbedder';
import { TypeFeatures } from 'src/types/fontkit';
export enum ParseSpeeds {
Fastest = Infinity,
Fast = 1500,
Medium = 500,
Slow = 100,
}
export interface AttachmentOptions extends EmbeddedFileOptions {}
export interface SaveOptions {
useObjectStreams?: boolean;
addDefaultPage?: boolean;
objectsPerTick?: number;
updateFieldAppearances?: boolean;
}
export interface Base64SaveOptions extends SaveOptions {
dataUri?: boolean;
}
export interface LoadOptions {
ignoreEncryption?: boolean;
parseSpeed?: ParseSpeeds | number;
throwOnInvalidObject?: boolean;
updateMetadata?: boolean;
capNumbers?: boolean;
}
export interface CreateOptions {
updateMetadata?: boolean;
}
export interface EmbedFontOptions {
subset?: boolean;
customName?: string;
features?: TypeFeatures;
}
export interface SetTitleOptions {
showInWindowTitleBar: boolean;
}

90
node_modules/pdf-lib/src/api/PDFEmbeddedFile.ts generated vendored Normal file
View File

@@ -0,0 +1,90 @@
import Embeddable from 'src/api/Embeddable';
import PDFDocument from 'src/api/PDFDocument';
import FileEmbedder from 'src/core/embedders/FileEmbedder';
import { PDFName, PDFArray, PDFDict, PDFHexString, PDFRef } from 'src/core';
/**
* Represents a file that has been embedded in a [[PDFDocument]].
*/
export default class PDFEmbeddedFile implements Embeddable {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFDocument.attach]] method, which will create
* instances of [[PDFEmbeddedFile]] for you.
*
* Create an instance of [[PDFEmbeddedFile]] 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: FileEmbedder) =>
new PDFEmbeddedFile(ref, doc, embedder);
/** The unique reference assigned to this embedded file within the document. */
readonly ref: PDFRef;
/** The document to which this embedded file belongs. */
readonly doc: PDFDocument;
private alreadyEmbedded = false;
private readonly embedder: FileEmbedder;
private constructor(ref: PDFRef, doc: PDFDocument, embedder: FileEmbedder) {
this.ref = ref;
this.doc = doc;
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 embeddable files get embedded.
*
* Embed this embeddable file in its document.
*
* @returns Resolves when the embedding is complete.
*/
async embed(): Promise<void> {
if (!this.alreadyEmbedded) {
const ref = await this.embedder.embedIntoContext(
this.doc.context,
this.ref,
);
if (!this.doc.catalog.has(PDFName.of('Names'))) {
this.doc.catalog.set(PDFName.of('Names'), this.doc.context.obj({}));
}
const Names = this.doc.catalog.lookup(PDFName.of('Names'), PDFDict);
if (!Names.has(PDFName.of('EmbeddedFiles'))) {
Names.set(PDFName.of('EmbeddedFiles'), this.doc.context.obj({}));
}
const EmbeddedFiles = Names.lookup(PDFName.of('EmbeddedFiles'), PDFDict);
if (!EmbeddedFiles.has(PDFName.of('Names'))) {
EmbeddedFiles.set(PDFName.of('Names'), this.doc.context.obj([]));
}
const EFNames = EmbeddedFiles.lookup(PDFName.of('Names'), PDFArray);
EFNames.push(PDFHexString.fromText(this.embedder.fileName));
EFNames.push(ref);
/**
* The AF-Tag is needed to achieve PDF-A3 compliance for embedded files
*
* The following document outlines the uses cases of the associated files (AF) tag.
* See:
* https://www.pdfa.org/wp-content/uploads/2018/10/PDF20_AN002-AF.pdf
*/
if (!this.doc.catalog.has(PDFName.of('AF'))) {
this.doc.catalog.set(PDFName.of('AF'), this.doc.context.obj([]));
}
const AF = this.doc.catalog.lookup(PDFName.of('AF'), PDFArray);
AF.push(ref);
this.alreadyEmbedded = true;
}
}
}

104
node_modules/pdf-lib/src/api/PDFEmbeddedPage.ts generated vendored Normal file
View File

@@ -0,0 +1,104 @@
import Embeddable from 'src/api/Embeddable';
import PDFDocument from 'src/api/PDFDocument';
import { PDFPageEmbedder, PDFRef } from 'src/core';
import { assertIs } from 'src/utils';
/**
* Represents a PDF page that has been embedded in a [[PDFDocument]].
*/
export default class PDFEmbeddedPage implements Embeddable {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFDocument.embedPdf]] and
* > [[PDFDocument.embedPage]] methods, which will create instances of
* > [[PDFEmbeddedPage]] for you.
*
* Create an instance of [[PDFEmbeddedPage]] from an existing ref and embedder
*
* @param ref The unique reference for this embedded page.
* @param doc The document to which the embedded page will belong.
* @param embedder The embedder that will be used to embed the page.
*/
static of = (ref: PDFRef, doc: PDFDocument, embedder: PDFPageEmbedder) =>
new PDFEmbeddedPage(ref, doc, embedder);
/** The unique reference assigned to this embedded page within the document. */
readonly ref: PDFRef;
/** The document to which this embedded page belongs. */
readonly doc: PDFDocument;
/** The width of this page in pixels. */
readonly width: number;
/** The height of this page in pixels. */
readonly height: number;
private alreadyEmbedded = false;
private readonly embedder: PDFPageEmbedder;
private constructor(
ref: PDFRef,
doc: PDFDocument,
embedder: PDFPageEmbedder,
) {
assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]);
assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]);
assertIs(embedder, 'embedder', [[PDFPageEmbedder, 'PDFPageEmbedder']]);
this.ref = ref;
this.doc = doc;
this.width = embedder.width;
this.height = embedder.height;
this.embedder = embedder;
}
/**
* Compute the width and height of this page after being scaled by the
* given `factor`. For example:
* ```js
* embeddedPage.width // => 500
* embeddedPage.height // => 250
*
* const scaled = embeddedPage.scale(0.5)
* scaled.width // => 250
* scaled.height // => 125
* ```
* This operation is often useful before drawing a page with
* [[PDFPage.drawPage]] to compute the `width` and `height` options.
* @param factor The factor by which this page should be scaled.
* @returns The width and height of the page after being scaled.
*/
scale(factor: number) {
assertIs(factor, 'factor', ['number']);
return { width: this.width * factor, height: this.height * factor };
}
/**
* Get the width and height of this page. For example:
* ```js
* const { width, height } = embeddedPage.size()
* ```
* @returns The width and height of the page.
*/
size() {
return this.scale(1);
}
/**
* > **NOTE:** You probably don't need to call this method directly. The
* > [[PDFDocument.save]] and [[PDFDocument.saveAsBase64]] methods will
* > automatically ensure all embeddable pages get embedded.
*
* Embed this embeddable page in its document.
*
* @returns Resolves when the embedding is complete.
*/
async embed(): Promise<void> {
if (!this.alreadyEmbedded) {
await this.embedder.embedIntoContext(this.doc.context, this.ref);
this.alreadyEmbedded = true;
}
}
}

154
node_modules/pdf-lib/src/api/PDFFont.ts generated vendored Normal file
View File

@@ -0,0 +1,154 @@
import Embeddable from 'src/api//Embeddable';
import PDFDocument from 'src/api/PDFDocument';
import {
CustomFontEmbedder,
PDFHexString,
PDFRef,
StandardFontEmbedder,
} from 'src/core';
import { assertIs, assertOrUndefined } from 'src/utils';
export type FontEmbedder = CustomFontEmbedder | StandardFontEmbedder;
/**
* Represents a font that has been embedded in a [[PDFDocument]].
*/
export default class PDFFont implements Embeddable {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFDocument.embedFont]] and
* > [[PDFDocument.embedStandardFont]] methods, which will create instances
* > of [[PDFFont]] for you.
*
* Create an instance of [[PDFFont]] from an existing ref and embedder
*
* @param ref The unique reference for this font.
* @param doc The document to which the font will belong.
* @param embedder The embedder that will be used to embed the font.
*/
static of = (ref: PDFRef, doc: PDFDocument, embedder: FontEmbedder) =>
new PDFFont(ref, doc, embedder);
/** The unique reference assigned to this font within the document. */
readonly ref: PDFRef;
/** The document to which this font belongs. */
readonly doc: PDFDocument;
/** The name of this font. */
readonly name: string;
private modified = true;
private readonly embedder: FontEmbedder;
private constructor(ref: PDFRef, doc: PDFDocument, embedder: FontEmbedder) {
assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]);
assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]);
assertIs(embedder, 'embedder', [
[CustomFontEmbedder, 'CustomFontEmbedder'],
[StandardFontEmbedder, 'StandardFontEmbedder'],
]);
this.ref = ref;
this.doc = doc;
this.name = embedder.fontName;
this.embedder = embedder;
}
/**
* > **NOTE:** You probably don't need to call this method directly. The
* > [[PDFPage.drawText]] method will automatically encode the text it is
* > given.
*
* Encodes a string of text in this font.
*
* @param text The text to be encoded.
* @returns The encoded text as a hex string.
*/
encodeText(text: string): PDFHexString {
assertIs(text, 'text', ['string']);
this.modified = true;
return this.embedder.encodeText(text);
}
/**
* Measure the width of a string of text drawn in this font at a given size.
* For example:
* ```js
* const width = font.widthOfTextAtSize('Foo Bar Qux Baz', 36)
* ```
* @param text The string of text to be measured.
* @param size The font size to be used for this measurement.
* @returns The width of the string of text when drawn in this font at the
* given size.
*/
widthOfTextAtSize(text: string, size: number): number {
assertIs(text, 'text', ['string']);
assertIs(size, 'size', ['number']);
return this.embedder.widthOfTextAtSize(text, size);
}
/**
* Measure the height of this font at a given size. For example:
* ```js
* const height = font.heightAtSize(24)
* ```
*
* The `options.descender` value controls whether or not the font's
* descender is included in the height calculation.
*
* @param size The font size to be used for this measurement.
* @param options The options to be used when computing this measurement.
* @returns The height of this font at the given size.
*/
heightAtSize(size: number, options?: { descender?: boolean }): number {
assertIs(size, 'size', ['number']);
assertOrUndefined(options?.descender, 'options.descender', ['boolean']);
return this.embedder.heightOfFontAtSize(size, {
descender: options?.descender ?? true,
});
}
/**
* Compute the font size at which this font is a given height. For example:
* ```js
* const fontSize = font.sizeAtHeight(12)
* ```
* @param height The height to be used for this calculation.
* @returns The font size at which this font is the given height.
*/
sizeAtHeight(height: number): number {
assertIs(height, 'height', ['number']);
return this.embedder.sizeOfFontAtHeight(height);
}
/**
* Get the set of unicode code points that can be represented by this font.
* @returns The set of unicode code points supported by this font.
*/
getCharacterSet(): number[] {
if (this.embedder instanceof StandardFontEmbedder) {
return this.embedder.encoding.supportedCodePoints;
} else {
return this.embedder.font.characterSet;
}
}
/**
* > **NOTE:** You probably don't need to call this method directly. The
* > [[PDFDocument.save]] and [[PDFDocument.saveAsBase64]] methods will
* > automatically ensure all fonts get embedded.
*
* Embed this font in its document.
*
* @returns Resolves when the embedding is complete.
*/
async embed(): Promise<void> {
// TODO: Cleanup orphan embedded objects if a font is embedded multiple times...
if (this.modified) {
await this.embedder.embedIntoContext(this.doc.context, this.ref);
this.modified = false;
}
}
}

143
node_modules/pdf-lib/src/api/PDFImage.ts generated vendored Normal file
View File

@@ -0,0 +1,143 @@
import Embeddable from 'src/api/Embeddable';
import PDFDocument from 'src/api/PDFDocument';
import { JpegEmbedder, PDFRef, PngEmbedder } from 'src/core';
import { assertIs } from 'src/utils';
export type ImageEmbedder = JpegEmbedder | PngEmbedder;
/**
* Represents an image that has been embedded in a [[PDFDocument]].
*/
export default class PDFImage implements Embeddable {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFDocument.embedPng]] and [[PDFDocument.embedJpg]]
* > methods, which will create instances of [[PDFImage]] for you.
*
* Create an instance of [[PDFImage]] from an existing ref and embedder
*
* @param ref The unique reference for this image.
* @param doc The document to which the image will belong.
* @param embedder The embedder that will be used to embed the image.
*/
static of = (ref: PDFRef, doc: PDFDocument, embedder: ImageEmbedder) =>
new PDFImage(ref, doc, embedder);
/** The unique reference assigned to this image within the document. */
readonly ref: PDFRef;
/** The document to which this image belongs. */
readonly doc: PDFDocument;
/** The width of this image in pixels. */
readonly width: number;
/** The height of this image in pixels. */
readonly height: number;
private embedder: ImageEmbedder | undefined;
private embedTask: Promise<PDFRef> | undefined;
private constructor(ref: PDFRef, doc: PDFDocument, embedder: ImageEmbedder) {
assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]);
assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]);
assertIs(embedder, 'embedder', [
[JpegEmbedder, 'JpegEmbedder'],
[PngEmbedder, 'PngEmbedder'],
]);
this.ref = ref;
this.doc = doc;
this.width = embedder.width;
this.height = embedder.height;
this.embedder = embedder;
}
/**
* Compute the width and height of this image after being scaled by the
* given `factor`. For example:
* ```js
* image.width // => 500
* image.height // => 250
*
* const scaled = image.scale(0.5)
* scaled.width // => 250
* scaled.height // => 125
* ```
* This operation is often useful before drawing an image with
* [[PDFPage.drawImage]] to compute the `width` and `height` options.
* @param factor The factor by which this image should be scaled.
* @returns The width and height of the image after being scaled.
*/
scale(factor: number) {
assertIs(factor, 'factor', ['number']);
return { width: this.width * factor, height: this.height * factor };
}
/**
* Get the width and height of this image after scaling it as large as
* possible while maintaining its aspect ratio and not exceeding the
* specified `width` and `height`. For example:
* ```
* image.width // => 500
* image.height // => 250
*
* const scaled = image.scaleToFit(750, 1000)
* scaled.width // => 750
* scaled.height // => 375
* ```
* The `width` and `height` parameters can also be thought of as the width
* and height of a box that the scaled image must fit within.
* @param width The bounding box's width.
* @param height The bounding box's height.
* @returns The width and height of the image after being scaled.
*/
scaleToFit(width: number, height: number) {
assertIs(width, 'width', ['number']);
assertIs(height, 'height', ['number']);
const imgWidthScale = width / this.width;
const imgHeightScale = height / this.height;
const scale = Math.min(imgWidthScale, imgHeightScale);
return this.scale(scale);
}
/**
* Get the width and height of this image. For example:
* ```js
* const { width, height } = image.size()
* ```
* @returns The width and height of the image.
*/
size() {
return this.scale(1);
}
/**
* > **NOTE:** You probably don't need to call this method directly. The
* > [[PDFDocument.save]] and [[PDFDocument.saveAsBase64]] methods will
* > automatically ensure all images get embedded.
*
* Embed this image in its document.
*
* @returns Resolves when the embedding is complete.
*/
async embed(): Promise<void> {
if (!this.embedder) return;
// The image 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.embedTask) {
const { doc, ref } = this;
this.embedTask = this.embedder.embedIntoContext(doc.context, ref);
}
await this.embedTask;
// We clear `this.embedder` so that the indirectly referenced image data
// can be garbage collected, thus avoiding a memory leak.
// See https://github.com/Hopding/pdf-lib/pull/1032/files.
this.embedder = undefined;
}
}

82
node_modules/pdf-lib/src/api/PDFJavaScript.ts generated vendored Normal file
View File

@@ -0,0 +1,82 @@
import Embeddable from 'src/api/Embeddable';
import PDFDocument from 'src/api/PDFDocument';
import JavaScriptEmbedder from 'src/core/embedders/JavaScriptEmbedder';
import { PDFName, PDFArray, PDFDict, PDFHexString, PDFRef } from 'src/core';
/**
* Represents JavaScript that has been embedded in a [[PDFDocument]].
*/
export default class PDFJavaScript implements Embeddable {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFDocument.addJavaScript]] method, which will
* create instances of [[PDFJavaScript]] for you.
*
* Create an instance of [[PDFJavaScript]] from an existing ref and script
*
* @param ref The unique reference for this script.
* @param doc The document to which the script will belong.
* @param embedder The embedder that will be used to embed the script.
*/
static of = (ref: PDFRef, doc: PDFDocument, embedder: JavaScriptEmbedder) =>
new PDFJavaScript(ref, doc, embedder);
/** The unique reference assigned to this embedded script within the document. */
readonly ref: PDFRef;
/** The document to which this embedded script belongs. */
readonly doc: PDFDocument;
private alreadyEmbedded = false;
private readonly embedder: JavaScriptEmbedder;
private constructor(
ref: PDFRef,
doc: PDFDocument,
embedder: JavaScriptEmbedder,
) {
this.ref = ref;
this.doc = doc;
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 JavaScripts get embedded.
*
* Embed this JavaScript in its document.
*
* @returns Resolves when the embedding is complete.
*/
async embed(): Promise<void> {
if (!this.alreadyEmbedded) {
const { catalog, context } = this.doc;
const ref = await this.embedder.embedIntoContext(
this.doc.context,
this.ref,
);
if (!catalog.has(PDFName.of('Names'))) {
catalog.set(PDFName.of('Names'), context.obj({}));
}
const Names = catalog.lookup(PDFName.of('Names'), PDFDict);
if (!Names.has(PDFName.of('JavaScript'))) {
Names.set(PDFName.of('JavaScript'), context.obj({}));
}
const Javascript = Names.lookup(PDFName.of('JavaScript'), PDFDict);
if (!Javascript.has(PDFName.of('Names'))) {
Javascript.set(PDFName.of('Names'), context.obj([]));
}
const JSNames = Javascript.lookup(PDFName.of('Names'), PDFArray);
JSNames.push(PDFHexString.fromText(this.embedder.scriptName));
JSNames.push(ref);
this.alreadyEmbedded = true;
}
}
}

1621
node_modules/pdf-lib/src/api/PDFPage.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

158
node_modules/pdf-lib/src/api/PDFPageOptions.ts generated vendored Normal file
View File

@@ -0,0 +1,158 @@
import { Color } from 'src/api/colors';
import PDFFont from 'src/api/PDFFont';
import { Rotation } from 'src/api/rotations';
import { LineCapStyle } from 'src/api/operators';
export enum BlendMode {
Normal = 'Normal',
Multiply = 'Multiply',
Screen = 'Screen',
Overlay = 'Overlay',
Darken = 'Darken',
Lighten = 'Lighten',
ColorDodge = 'ColorDodge',
ColorBurn = 'ColorBurn',
HardLight = 'HardLight',
SoftLight = 'SoftLight',
Difference = 'Difference',
Exclusion = 'Exclusion',
}
export interface PDFPageDrawTextOptions {
color?: Color;
opacity?: number;
blendMode?: BlendMode;
font?: PDFFont;
size?: number;
rotate?: Rotation;
xSkew?: Rotation;
ySkew?: Rotation;
x?: number;
y?: number;
lineHeight?: number;
maxWidth?: number;
wordBreaks?: string[];
}
export interface PDFPageDrawImageOptions {
x?: number;
y?: number;
width?: number;
height?: number;
rotate?: Rotation;
xSkew?: Rotation;
ySkew?: Rotation;
opacity?: number;
blendMode?: BlendMode;
}
export interface PDFPageDrawPageOptions {
x?: number;
y?: number;
xScale?: number;
yScale?: number;
width?: number;
height?: number;
rotate?: Rotation;
xSkew?: Rotation;
ySkew?: Rotation;
opacity?: number;
blendMode?: BlendMode;
}
export interface PDFPageDrawSVGOptions {
x?: number;
y?: number;
scale?: number;
rotate?: Rotation;
borderWidth?: number;
color?: Color;
opacity?: number;
borderColor?: Color;
borderOpacity?: number;
borderDashArray?: number[];
borderDashPhase?: number;
borderLineCap?: LineCapStyle;
blendMode?: BlendMode;
}
export interface PDFPageDrawLineOptions {
start: { x: number; y: number };
end: { x: number; y: number };
thickness?: number;
color?: Color;
opacity?: number;
lineCap?: LineCapStyle;
dashArray?: number[];
dashPhase?: number;
blendMode?: BlendMode;
}
export interface PDFPageDrawRectangleOptions {
x?: number;
y?: number;
width?: number;
height?: number;
rotate?: Rotation;
xSkew?: Rotation;
ySkew?: Rotation;
borderWidth?: number;
color?: Color;
opacity?: number;
borderColor?: Color;
borderOpacity?: number;
borderDashArray?: number[];
borderDashPhase?: number;
borderLineCap?: LineCapStyle;
blendMode?: BlendMode;
}
export interface PDFPageDrawSquareOptions {
x?: number;
y?: number;
size?: number;
rotate?: Rotation;
xSkew?: Rotation;
ySkew?: Rotation;
borderWidth?: number;
color?: Color;
opacity?: number;
borderColor?: Color;
borderOpacity?: number;
borderDashArray?: number[];
borderDashPhase?: number;
borderLineCap?: LineCapStyle;
blendMode?: BlendMode;
}
export interface PDFPageDrawEllipseOptions {
x?: number;
y?: number;
xScale?: number;
yScale?: number;
rotate?: Rotation;
color?: Color;
opacity?: number;
borderColor?: Color;
borderOpacity?: number;
borderWidth?: number;
borderDashArray?: number[];
borderDashPhase?: number;
borderLineCap?: LineCapStyle;
blendMode?: BlendMode;
}
export interface PDFPageDrawCircleOptions {
x?: number;
y?: number;
size?: number;
color?: Color;
opacity?: number;
borderColor?: Color;
borderOpacity?: number;
borderWidth?: number;
borderDashArray?: number[];
borderDashPhase?: number;
borderLineCap?: LineCapStyle;
blendMode?: BlendMode;
}

16
node_modules/pdf-lib/src/api/StandardFonts.ts generated vendored Normal file
View File

@@ -0,0 +1,16 @@
export enum StandardFonts {
Courier = 'Courier',
CourierBold = 'Courier-Bold',
CourierOblique = 'Courier-Oblique',
CourierBoldOblique = 'Courier-BoldOblique',
Helvetica = 'Helvetica',
HelveticaBold = 'Helvetica-Bold',
HelveticaOblique = 'Helvetica-Oblique',
HelveticaBoldOblique = 'Helvetica-BoldOblique',
TimesRoman = 'Times-Roman',
TimesRomanBold = 'Times-Bold',
TimesRomanItalic = 'Times-Italic',
TimesRomanBoldItalic = 'Times-BoldItalic',
Symbol = 'Symbol',
ZapfDingbats = 'ZapfDingbats',
}

104
node_modules/pdf-lib/src/api/colors.ts generated vendored Normal file
View File

@@ -0,0 +1,104 @@
import {
setFillingCmykColor,
setFillingGrayscaleColor,
setFillingRgbColor,
setStrokingCmykColor,
setStrokingGrayscaleColor,
setStrokingRgbColor,
} from 'src/api/operators';
import { assertRange, error } from 'src/utils';
export enum ColorTypes {
Grayscale = 'Grayscale',
RGB = 'RGB',
CMYK = 'CMYK',
}
export interface Grayscale {
type: ColorTypes.Grayscale;
gray: number;
}
export interface RGB {
type: ColorTypes.RGB;
red: number;
green: number;
blue: number;
}
export interface CMYK {
type: ColorTypes.CMYK;
cyan: number;
magenta: number;
yellow: number;
key: number;
}
export type Color = Grayscale | RGB | CMYK;
export const grayscale = (gray: number): Grayscale => {
assertRange(gray, 'gray', 0.0, 1.0);
return { type: ColorTypes.Grayscale, gray };
};
export const rgb = (red: number, green: number, blue: number): RGB => {
assertRange(red, 'red', 0, 1);
assertRange(green, 'green', 0, 1);
assertRange(blue, 'blue', 0, 1);
return { type: ColorTypes.RGB, red, green, blue };
};
export const cmyk = (
cyan: number,
magenta: number,
yellow: number,
key: number,
): CMYK => {
assertRange(cyan, 'cyan', 0, 1);
assertRange(magenta, 'magenta', 0, 1);
assertRange(yellow, 'yellow', 0, 1);
assertRange(key, 'key', 0, 1);
return { type: ColorTypes.CMYK, cyan, magenta, yellow, key };
};
const { Grayscale, RGB, CMYK } = ColorTypes;
// 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)
: error(`Invalid color: ${JSON.stringify(color)}`);
// 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)
: error(`Invalid color: ${JSON.stringify(color)}`);
// prettier-ignore
export const componentsToColor = (comps?: number[], scale = 1) => (
comps?.length === 1 ? grayscale(
comps[0] * scale,
)
: comps?.length === 3 ? rgb(
comps[0] * scale,
comps[1] * scale,
comps[2] * scale,
)
: comps?.length === 4 ? cmyk(
comps[0] * scale,
comps[1] * scale,
comps[2] * scale,
comps[3] * scale,
)
: undefined
);
// prettier-ignore
export const colorToComponents = (color: Color) =>
color.type === Grayscale ? [color.gray]
: color.type === RGB ? [color.red, color.green, color.blue]
: color.type === CMYK ? [color.cyan, color.magenta, color.yellow, color.key]
: error(`Invalid color: ${JSON.stringify(color)}`);

111
node_modules/pdf-lib/src/api/errors.ts generated vendored Normal file
View File

@@ -0,0 +1,111 @@
// tslint:disable: max-classes-per-file
// TODO: Include link to documentation with example
export class EncryptedPDFError extends Error {
constructor() {
const msg =
'Input document to `PDFDocument.load` is encrypted. You can use `PDFDocument.load(..., { ignoreEncryption: true })` if you wish to load the document anyways.';
super(msg);
}
}
// TODO: Include link to documentation with example
export class FontkitNotRegisteredError extends Error {
constructor() {
const msg =
'Input to `PDFDocument.embedFont` was a custom font, but no `fontkit` instance was found. You must register a `fontkit` instance with `PDFDocument.registerFontkit(...)` before embedding custom fonts.';
super(msg);
}
}
// TODO: Include link to documentation with example
export class ForeignPageError extends Error {
constructor() {
const msg =
'A `page` passed to `PDFDocument.addPage` or `PDFDocument.insertPage` was from a different (foreign) PDF document. If you want to copy pages from one PDFDocument to another, you must use `PDFDocument.copyPages(...)` to copy the pages before adding or inserting them.';
super(msg);
}
}
// TODO: Include link to documentation with example
export class RemovePageFromEmptyDocumentError extends Error {
constructor() {
const msg =
'PDFDocument has no pages so `PDFDocument.removePage` cannot be called';
super(msg);
}
}
export class NoSuchFieldError extends Error {
constructor(name: string) {
const msg = `PDFDocument has no form field with the name "${name}"`;
super(msg);
}
}
export class UnexpectedFieldTypeError extends Error {
constructor(name: string, expected: any, actual: any) {
const expectedType = expected?.name;
const actualType = actual?.constructor?.name ?? actual;
const msg =
`Expected field "${name}" to be of type ${expectedType}, ` +
`but it is actually of type ${actualType}`;
super(msg);
}
}
export class MissingOnValueCheckError extends Error {
constructor(onValue: any) {
const msg = `Failed to select check box due to missing onValue: "${onValue}"`;
super(msg);
}
}
export class FieldAlreadyExistsError extends Error {
constructor(name: string) {
const msg = `A field already exists with the specified name: "${name}"`;
super(msg);
}
}
export class InvalidFieldNamePartError extends Error {
constructor(namePart: string) {
const msg = `Field name contains invalid component: "${namePart}"`;
super(msg);
}
}
export class FieldExistsAsNonTerminalError extends Error {
constructor(name: string) {
const msg = `A non-terminal field already exists with the specified name: "${name}"`;
super(msg);
}
}
export class RichTextFieldReadError extends Error {
constructor(fieldName: string) {
const msg = `Reading rich text fields is not supported: Attempted to read rich text field: ${fieldName}`;
super(msg);
}
}
export class CombedTextLayoutError extends Error {
constructor(lineLength: number, cellCount: number) {
const msg = `Failed to layout combed text as lineLength=${lineLength} is greater than cellCount=${cellCount}`;
super(msg);
}
}
export class ExceededMaxLengthError extends Error {
constructor(textLength: number, maxLength: number, name: string) {
const msg = `Attempted to set text with length=${textLength} for TextField with maxLength=${maxLength} and name=${name}`;
super(msg);
}
}
export class InvalidMaxLengthError extends Error {
constructor(textLength: number, maxLength: number, name: string) {
const msg = `Attempted to set maxLength=${maxLength}, which is less than ${textLength}, the length of this field's current value (name=${name})`;
super(msg);
}
}

267
node_modules/pdf-lib/src/api/form/PDFButton.ts generated vendored Normal file
View File

@@ -0,0 +1,267 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFPage from 'src/api/PDFPage';
import PDFFont from 'src/api/PDFFont';
import PDFImage from 'src/api/PDFImage';
import { ImageAlignment } from 'src/api/image/alignment';
import {
AppearanceProviderFor,
normalizeAppearance,
defaultButtonAppearanceProvider,
} from 'src/api/form/appearances';
import PDFField, {
FieldAppearanceOptions,
assertFieldAppearanceOptions,
} from 'src/api/form/PDFField';
import { rgb } from 'src/api/colors';
import { degrees } from 'src/api/rotations';
import {
PDFRef,
PDFStream,
PDFAcroPushButton,
PDFWidgetAnnotation,
} from 'src/core';
import { assertIs, assertOrUndefined, assertPositive } from 'src/utils';
/**
* Represents a button field of a [[PDFForm]].
*
* [[PDFButton]] fields are interactive controls that users can click with their
* mouse. This type of [[PDFField]] is not stateful. The purpose of a button
* is to perform an action when the user clicks on it, such as opening a print
* modal or resetting the form. Buttons are typically rectangular in shape and
* have a text label describing the action that they perform when clicked.
*/
export default class PDFButton extends PDFField {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFForm.getButton]] method, which will create an
* > instance of [[PDFButton]] for you.
*
* Create an instance of [[PDFButton]] from an existing acroPushButton and ref
*
* @param acroPushButton The underlying `PDFAcroPushButton` for this button.
* @param ref The unique reference for this button.
* @param doc The document to which this button will belong.
*/
static of = (
acroPushButton: PDFAcroPushButton,
ref: PDFRef,
doc: PDFDocument,
) => new PDFButton(acroPushButton, ref, doc);
/** The low-level PDFAcroPushButton wrapped by this button. */
readonly acroField: PDFAcroPushButton;
private constructor(
acroPushButton: PDFAcroPushButton,
ref: PDFRef,
doc: PDFDocument,
) {
super(acroPushButton, ref, doc);
assertIs(acroPushButton, 'acroButton', [
[PDFAcroPushButton, 'PDFAcroPushButton'],
]);
this.acroField = acroPushButton;
}
/**
* Display an image inside the bounds of this button's widgets. For example:
* ```js
* const pngImage = await pdfDoc.embedPng(...)
* const button = form.getButton('some.button.field')
* button.setImage(pngImage, ImageAlignment.Center)
* ```
* This will update the appearances streams for each of this button's widgets.
* @param image The image that should be displayed.
* @param alignment The alignment of the image.
*/
setImage(image: PDFImage, alignment = ImageAlignment.Center) {
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const streamRef = this.createImageAppearanceStream(
widget,
image,
alignment,
);
this.updateWidgetAppearances(widget, { normal: streamRef });
}
this.markAsClean();
}
/**
* Set the font size for this field. Larger font sizes will result in larger
* text being displayed when PDF readers render this button. Font sizes may
* be integer or floating point numbers. Supplying a negative font size will
* cause this method to throw an error.
*
* For example:
* ```js
* const button = form.getButton('some.button.field')
* button.setFontSize(4)
* button.setFontSize(15.7)
* ```
*
* > This method depends upon the existence of a default appearance
* > (`/DA`) string. If this field does not have a default appearance string,
* > or that string does not contain a font size (via the `Tf` operator),
* > then this method will throw an error.
*
* @param fontSize The font size to be used when rendering text in this field.
*/
setFontSize(fontSize: number) {
assertPositive(fontSize, 'fontSize');
this.acroField.setFontSize(fontSize);
this.markAsDirty();
}
/**
* Show this button on the specified page with the given text. For example:
* ```js
* const ubuntuFont = await pdfDoc.embedFont(ubuntuFontBytes)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const button = form.createButton('some.button.field')
*
* button.addToPage('Do Stuff', page, {
* x: 50,
* y: 75,
* width: 200,
* height: 100,
* textColor: rgb(1, 0, 0),
* backgroundColor: rgb(0, 1, 0),
* borderColor: rgb(0, 0, 1),
* borderWidth: 2,
* rotate: degrees(90),
* font: ubuntuFont,
* })
* ```
* This will create a new widget for this button field.
* @param text The text to be displayed for this button widget.
* @param page The page to which this button widget should be added.
* @param options The options to be used when adding this button widget.
*/
addToPage(
// TODO: This needs to be optional, e.g. for image buttons
text: string,
page: PDFPage,
options?: FieldAppearanceOptions,
) {
assertOrUndefined(text, 'text', ['string']);
assertOrUndefined(page, 'page', [[PDFPage, 'PDFPage']]);
assertFieldAppearanceOptions(options);
// Create a widget for this button
const widget = this.createWidget({
x: (options?.x ?? 0) - (options?.borderWidth ?? 0) / 2,
y: (options?.y ?? 0) - (options?.borderWidth ?? 0) / 2,
width: options?.width ?? 100,
height: options?.height ?? 50,
textColor: options?.textColor ?? rgb(0, 0, 0),
backgroundColor: options?.backgroundColor ?? rgb(0.75, 0.75, 0.75),
borderColor: options?.borderColor,
borderWidth: options?.borderWidth ?? 0,
rotate: options?.rotate ?? degrees(0),
caption: text,
hidden: options?.hidden,
page: page.ref,
});
const widgetRef = this.doc.context.register(widget.dict);
// Add widget to this field
this.acroField.addWidget(widgetRef);
// Set appearance streams for widget
const font = options?.font ?? this.doc.getForm().getDefaultFont();
this.updateWidgetAppearance(widget, font);
// Add widget to the given page
page.node.addAnnot(widgetRef);
}
/**
* Returns `true` if this button has been marked as dirty, or if any of this
* button's widgets do not have an appearance stream. For example:
* ```js
* const button = form.getButton('some.button.field')
* if (button.needsAppearancesUpdate()) console.log('Needs update')
* ```
* @returns Whether or not this button needs an appearance update.
*/
needsAppearancesUpdate(): boolean {
if (this.isDirty()) return true;
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const hasAppearances =
widget.getAppearances()?.normal instanceof PDFStream;
if (!hasAppearances) return true;
}
return false;
}
/**
* Update the appearance streams for each of this button's widgets using
* the default appearance provider for buttons. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const button = form.getButton('some.button.field')
* button.defaultUpdateAppearances(helvetica)
* ```
* @param font The font to be used for creating the appearance streams.
*/
defaultUpdateAppearances(font: PDFFont) {
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
this.updateAppearances(font);
}
/**
* Update the appearance streams for each of this button's widgets using
* the given appearance provider. If no `provider` is passed, the default
* appearance provider for buttons will be used. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const button = form.getButton('some.button.field')
* button.updateAppearances(helvetica, (field, widget, font) => {
* ...
* return {
* normal: drawButton(...),
* down: drawButton(...),
* }
* })
* ```
* @param font The font to be used for creating the appearance streams.
* @param provider Optionally, the appearance provider to be used for
* generating the contents of the appearance streams.
*/
updateAppearances(
font: PDFFont,
provider?: AppearanceProviderFor<PDFButton>,
) {
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
assertOrUndefined(provider, 'provider', [Function]);
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
this.updateWidgetAppearance(widget, font, provider);
}
}
private updateWidgetAppearance(
widget: PDFWidgetAnnotation,
font: PDFFont,
provider?: AppearanceProviderFor<PDFButton>,
) {
const apProvider = provider ?? defaultButtonAppearanceProvider;
const appearances = normalizeAppearance(apProvider(this, widget, font));
this.updateWidgetAppearanceWithFont(widget, font, appearances);
}
}

262
node_modules/pdf-lib/src/api/form/PDFCheckBox.ts generated vendored Normal file
View File

@@ -0,0 +1,262 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFPage from 'src/api/PDFPage';
import {
AppearanceProviderFor,
normalizeAppearance,
defaultCheckBoxAppearanceProvider,
} from 'src/api/form/appearances';
import { rgb } from 'src/api/colors';
import { degrees } from 'src/api/rotations';
import PDFField, {
FieldAppearanceOptions,
assertFieldAppearanceOptions,
} from 'src/api/form/PDFField';
import {
PDFName,
PDFRef,
PDFDict,
PDFAcroCheckBox,
PDFWidgetAnnotation,
} from 'src/core';
import { assertIs, assertOrUndefined } from 'src/utils';
/**
* Represents a check box field of a [[PDFForm]].
*
* [[PDFCheckBox]] fields are interactive boxes that users can click with their
* mouse. This type of [[PDFField]] has two states: `on` and `off`. The purpose
* of a check box is to enable users to select from one or more options, where
* each option is represented by a single check box. Check boxes are typically
* square in shape and display a check mark when they are in the `on` state.
*/
export default class PDFCheckBox extends PDFField {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFForm.getCheckBox]] method, which will create an
* > instance of [[PDFCheckBox]] for you.
*
* Create an instance of [[PDFCheckBox]] from an existing acroCheckBox and ref
*
* @param acroCheckBox The underlying `PDFAcroCheckBox` for this check box.
* @param ref The unique reference for this check box.
* @param doc The document to which this check box will belong.
*/
static of = (acroCheckBox: PDFAcroCheckBox, ref: PDFRef, doc: PDFDocument) =>
new PDFCheckBox(acroCheckBox, ref, doc);
/** The low-level PDFAcroCheckBox wrapped by this check box. */
readonly acroField: PDFAcroCheckBox;
private constructor(
acroCheckBox: PDFAcroCheckBox,
ref: PDFRef,
doc: PDFDocument,
) {
super(acroCheckBox, ref, doc);
assertIs(acroCheckBox, 'acroCheckBox', [
[PDFAcroCheckBox, 'PDFAcroCheckBox'],
]);
this.acroField = acroCheckBox;
}
/**
* Mark this check box. This operation is analogous to a human user clicking
* a check box to fill it in a PDF reader. This method will update the
* underlying state of the check box field to indicate it has been selected.
* PDF libraries and readers will be able to extract this value from the
* saved document and determine that it was selected.
*
* For example:
* ```js
* const checkBox = form.getCheckBox('some.checkBox.field')
* checkBox.check()
* ```
*
* This method will mark this check box as dirty, causing its appearance
* streams to be updated when either [[PDFDocument.save]] or
* [[PDFForm.updateFieldAppearances]] is called. The updated appearance
* streams will display a check mark inside the widgets of this check box
* field.
*/
check() {
const onValue = this.acroField.getOnValue() ?? PDFName.of('Yes');
this.markAsDirty();
this.acroField.setValue(onValue);
}
/**
* Clears this check box. This operation is analogous to a human user clicking
* a check box to unmark it in a PDF reader. This method will update the
* underlying state of the check box field to indicate it has been deselected.
* PDF libraries and readers will be able to extract this value from the
* saved document and determine that it was not selected.
*
* For example:
* ```js
* const checkBox = form.getCheckBox('some.checkBox.field')
* checkBox.uncheck()
* ```
*
* This method will mark this check box as dirty. See [[PDFCheckBox.check]]
* for more details about what this means.
*/
uncheck() {
this.markAsDirty();
this.acroField.setValue(PDFName.of('Off'));
}
/**
* Returns `true` if this check box is selected (either by a human user via
* a PDF reader, or else programmatically via software). For example:
* ```js
* const checkBox = form.getCheckBox('some.checkBox.field')
* if (checkBox.isChecked()) console.log('check box is selected')
* ```
* @returns Whether or not this check box is selected.
*/
isChecked(): boolean {
const onValue = this.acroField.getOnValue();
return !!onValue && onValue === this.acroField.getValue();
}
/**
* Show this check box on the specified page. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const checkBox = form.createCheckBox('some.checkBox.field')
*
* checkBox.addToPage(page, {
* x: 50,
* y: 75,
* width: 25,
* height: 25,
* textColor: rgb(1, 0, 0),
* backgroundColor: rgb(0, 1, 0),
* borderColor: rgb(0, 0, 1),
* borderWidth: 2,
* rotate: degrees(90),
* })
* ```
* This will create a new widget for this check box field.
* @param page The page to which this check box widget should be added.
* @param options The options to be used when adding this check box widget.
*/
addToPage(page: PDFPage, options?: FieldAppearanceOptions) {
assertIs(page, 'page', [[PDFPage, 'PDFPage']]);
assertFieldAppearanceOptions(options);
if (!options) options = {};
if (!('textColor' in options)) options.textColor = rgb(0, 0, 0);
if (!('backgroundColor' in options)) options.backgroundColor = rgb(1, 1, 1);
if (!('borderColor' in options)) options.borderColor = rgb(0, 0, 0);
if (!('borderWidth' in options)) options.borderWidth = 1;
// Create a widget for this check box
const widget = this.createWidget({
x: options.x ?? 0,
y: options.y ?? 0,
width: options.width ?? 50,
height: options.height ?? 50,
textColor: options.textColor,
backgroundColor: options.backgroundColor,
borderColor: options.borderColor,
borderWidth: options.borderWidth ?? 0,
rotate: options.rotate ?? degrees(0),
hidden: options.hidden,
page: page.ref,
});
const widgetRef = this.doc.context.register(widget.dict);
// Add widget to this field
this.acroField.addWidget(widgetRef);
// Set appearance streams for widget
widget.setAppearanceState(PDFName.of('Off'));
this.updateWidgetAppearance(widget, PDFName.of('Yes'));
// Add widget to the given page
page.node.addAnnot(widgetRef);
}
/**
* Returns `true` if any of this check box's widgets do not have an
* appearance stream for its current state. For example:
* ```js
* const checkBox = form.getCheckBox('some.checkBox.field')
* if (checkBox.needsAppearancesUpdate()) console.log('Needs update')
* ```
* @returns Whether or not this check box needs an appearance update.
*/
needsAppearancesUpdate(): boolean {
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const state = widget.getAppearanceState();
const normal = widget.getAppearances()?.normal;
if (!(normal instanceof PDFDict)) return true;
if (state && !normal.has(state)) return true;
}
return false;
}
/**
* Update the appearance streams for each of this check box's widgets using
* the default appearance provider for check boxes. For example:
* ```js
* const checkBox = form.getCheckBox('some.checkBox.field')
* checkBox.defaultUpdateAppearances()
* ```
*/
defaultUpdateAppearances() {
this.updateAppearances();
}
/**
* Update the appearance streams for each of this check box's widgets using
* the given appearance provider. If no `provider` is passed, the default
* appearance provider for check boxs will be used. For example:
* ```js
* const checkBox = form.getCheckBox('some.checkBox.field')
* checkBox.updateAppearances((field, widget) => {
* ...
* return {
* normal: { on: drawCheckBox(...), off: drawCheckBox(...) },
* down: { on: drawCheckBox(...), off: drawCheckBox(...) },
* }
* })
* ```
* @param provider Optionally, the appearance provider to be used for
* generating the contents of the appearance streams.
*/
updateAppearances(provider?: AppearanceProviderFor<PDFCheckBox>) {
assertOrUndefined(provider, 'provider', [Function]);
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const onValue = widget.getOnValue() ?? PDFName.of('Yes');
if (!onValue) continue;
this.updateWidgetAppearance(widget, onValue, provider);
}
this.markAsClean();
}
private updateWidgetAppearance(
widget: PDFWidgetAnnotation,
onValue: PDFName,
provider?: AppearanceProviderFor<PDFCheckBox>,
) {
const apProvider = provider ?? defaultCheckBoxAppearanceProvider;
const appearances = normalizeAppearance(apProvider(this, widget));
this.updateOnOffWidgetAppearance(widget, onValue, appearances);
}
}

652
node_modules/pdf-lib/src/api/form/PDFDropdown.ts generated vendored Normal file
View File

@@ -0,0 +1,652 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFPage from 'src/api/PDFPage';
import PDFFont from 'src/api/PDFFont';
import PDFField, {
FieldAppearanceOptions,
assertFieldAppearanceOptions,
} from 'src/api/form/PDFField';
import {
AppearanceProviderFor,
normalizeAppearance,
defaultDropdownAppearanceProvider,
} from 'src/api/form/appearances';
import { rgb } from 'src/api/colors';
import { degrees } from 'src/api/rotations';
import {
PDFHexString,
PDFRef,
PDFString,
PDFStream,
PDFWidgetAnnotation,
PDFAcroComboBox,
AcroChoiceFlags,
} from 'src/core';
import { assertIs, assertOrUndefined, assertPositive } from 'src/utils';
/**
* Represents a dropdown field of a [[PDFForm]].
*
* [[PDFDropdown]] fields are interactive text boxes that display a single
* element (the currently selected value). The purpose of a dropdown is to
* enable users to select a single option from a set of possible options. Users
* can click on a dropdown to view the full list of options it provides.
* Clicking on an option in the list will cause it to be selected and displayed
* in the dropdown's text box. Some dropdowns allow users to enter text
* directly into the box from their keyboard, rather than only being allowed to
* choose an option from the list (see [[PDFDropdown.isEditable]]).
*/
export default class PDFDropdown extends PDFField {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFForm.getDropdown]] method, which will create an
* > instance of [[PDFDropdown]] for you.
*
* Create an instance of [[PDFDropdown]] from an existing acroComboBox and ref
*
* @param acroComboBox The underlying `PDFAcroComboBox` for this dropdown.
* @param ref The unique reference for this dropdown.
* @param doc The document to which this dropdown will belong.
*/
static of = (acroComboBox: PDFAcroComboBox, ref: PDFRef, doc: PDFDocument) =>
new PDFDropdown(acroComboBox, ref, doc);
/** The low-level PDFAcroComboBox wrapped by this dropdown. */
readonly acroField: PDFAcroComboBox;
private constructor(
acroComboBox: PDFAcroComboBox,
ref: PDFRef,
doc: PDFDocument,
) {
super(acroComboBox, ref, doc);
assertIs(acroComboBox, 'acroComboBox', [
[PDFAcroComboBox, 'PDFAcroComboBox'],
]);
this.acroField = acroComboBox;
}
/**
* Get the list of available options for this dropdown. These options will be
* displayed to users who click on this dropdown in a PDF reader.
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* const options = dropdown.getOptions()
* console.log('Dropdown options:', options)
* ```
* @returns The options for this dropdown.
*/
getOptions(): string[] {
const rawOptions = this.acroField.getOptions();
const options = new Array<string>(rawOptions.length);
for (let idx = 0, len = options.length; idx < len; idx++) {
const { display, value } = rawOptions[idx];
options[idx] = (display ?? value).decodeText();
}
return options;
}
/**
* Get the selected options for this dropdown. These are the values that were
* selected by a human user via a PDF reader, or programatically via
* software.
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* const selections = dropdown.getSelected()
* console.log('Dropdown selections:', selections)
* ```
* > **NOTE:** Note that PDF readers only display one selected option when
* > rendering dropdowns. However, the PDF specification does allow for
* > multiple values to be selected in a dropdown. As such, the `pdf-lib`
* > API supports this. However, in most cases the array returned by this
* > method will contain only a single element (or no elements).
* @returns The selected options in this dropdown.
*/
getSelected(): string[] {
const values = this.acroField.getValues();
const selected = new Array<string>(values.length);
for (let idx = 0, len = values.length; idx < len; idx++) {
selected[idx] = values[idx].decodeText();
}
return selected;
}
/**
* Set the list of options that are available for this dropdown. These are
* the values that will be available for users to select when they view this
* dropdown in a PDF reader. Note that preexisting options for this dropdown
* will be removed. Only the values passed as `options` will be available to
* select.
* For example:
* ```js
* const dropdown = form.getDropdown('planets.dropdown')
* dropdown.setOptions(['Earth', 'Mars', 'Pluto', 'Venus'])
* ```
* @param options The options that should be available in this dropdown.
*/
setOptions(options: string[]) {
assertIs(options, 'options', [Array]);
const optionObjects = new Array<{ value: PDFHexString }>(options.length);
for (let idx = 0, len = options.length; idx < len; idx++) {
optionObjects[idx] = { value: PDFHexString.fromText(options[idx]) };
}
this.acroField.setOptions(optionObjects);
}
/**
* Add to the list of options that are available for this dropdown. Users
* will be able to select these values in a PDF reader. In addition to the
* values passed as `options`, any preexisting options for this dropdown will
* still be available for users to select.
* For example:
* ```js
* const dropdown = form.getDropdown('rockets.dropdown')
* dropdown.addOptions(['Saturn IV', 'Falcon Heavy'])
* ```
* @param options New options that should be available in this dropdown.
*/
addOptions(options: string | string[]) {
assertIs(options, 'options', ['string', Array]);
const optionsArr = Array.isArray(options) ? options : [options];
const existingOptions: {
value: PDFString | PDFHexString;
display?: PDFString | PDFHexString;
}[] = this.acroField.getOptions();
const newOptions = new Array<{ value: PDFHexString }>(optionsArr.length);
for (let idx = 0, len = optionsArr.length; idx < len; idx++) {
newOptions[idx] = { value: PDFHexString.fromText(optionsArr[idx]) };
}
this.acroField.setOptions(existingOptions.concat(newOptions));
}
/**
* Select one or more values for this dropdown. This operation is analogous
* to a human user opening the dropdown in a PDF reader and clicking on a
* value to select it. This method will update the underlying state of the
* dropdown to indicate which values have been selected. PDF libraries and
* readers will be able to extract these values from the saved document and
* determine which values were selected.
*
* For example:
* ```js
* const dropdown = form.getDropdown('best.superhero.dropdown')
* dropdown.select('One Punch Man')
* ```
*
* This method will mark this dropdown as dirty, causing its appearance
* streams to be updated when either [[PDFDocument.save]] or
* [[PDFForm.updateFieldAppearances]] is called. The updated streams will
* display the selected option inside the widgets of this dropdown.
*
* **IMPORTANT:** The default font used to update appearance streams is
* [[StandardFonts.Helvetica]]. Note that this is a WinAnsi font. This means
* that encoding errors will be thrown if the selected option for this field
* contains characters outside the WinAnsi character set (the latin alphabet).
*
* Embedding a custom font and passing it to
* [[PDFForm.updateFieldAppearances]] or [[PDFDropdown.updateAppearances]]
* allows you to generate appearance streams with characters outside the
* latin alphabet (assuming the custom font supports them).
*
* Selecting an option that does not exist in this dropdown's option list
* (see [[PDFDropdown.getOptions]]) will enable editing on this dropdown
* (see [[PDFDropdown.enableEditing]]).
*
* > **NOTE:** PDF readers only display one selected option when rendering
* > dropdowns. However, the PDF specification does allow for multiple values
* > to be selected in a dropdown. As such, the `pdf-lib` API supports this.
* > However, it is not recommended to select more than one value with this
* > method, as only one will be visible. [[PDFOptionList]] fields are better
* > suited for displaying multiple selected values.
*
* @param options The options to be selected.
* @param merge Whether or not existing selections should be preserved.
*/
select(options: string | string[], merge = false) {
assertIs(options, 'options', ['string', Array]);
assertIs(merge, 'merge', ['boolean']);
const optionsArr = Array.isArray(options) ? options : [options];
const validOptions = this.getOptions();
const hasCustomOption = optionsArr.find(
(option) => !validOptions.includes(option),
);
if (hasCustomOption) this.enableEditing();
this.markAsDirty();
if (optionsArr.length > 1 || (optionsArr.length === 1 && merge)) {
this.enableMultiselect();
}
const values = new Array<PDFHexString>(optionsArr.length);
for (let idx = 0, len = optionsArr.length; idx < len; idx++) {
values[idx] = PDFHexString.fromText(optionsArr[idx]);
}
if (merge) {
const existingValues = this.acroField.getValues();
this.acroField.setValues(existingValues.concat(values));
} else {
this.acroField.setValues(values);
}
}
/**
* Clear all selected values for this dropdown. This operation is equivalent
* to selecting an empty list. This method will update the underlying state
* of the dropdown to indicate that no values have been selected.
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.clear()
* ```
* This method will mark this text field as dirty. See [[PDFDropdown.select]]
* for more details about what this means.
*/
clear() {
this.markAsDirty();
this.acroField.setValues([]);
}
/**
* Set the font size for this field. Larger font sizes will result in larger
* text being displayed when PDF readers render this dropdown. Font sizes may
* be integer or floating point numbers. Supplying a negative font size will
* cause this method to throw an error.
*
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.setFontSize(4)
* dropdown.setFontSize(15.7)
* ```
*
* > This method depends upon the existence of a default appearance
* > (`/DA`) string. If this field does not have a default appearance string,
* > or that string does not contain a font size (via the `Tf` operator),
* > then this method will throw an error.
*
* @param fontSize The font size to be used when rendering text in this field.
*/
setFontSize(fontSize: number) {
assertPositive(fontSize, 'fontSize');
this.acroField.setFontSize(fontSize);
this.markAsDirty();
}
/**
* Returns `true` if users are allowed to edit the selected value of this
* dropdown directly and are not constrained by the list of available
* options. See [[PDFDropdown.enableEditing]] and
* [[PDFDropdown.disableEditing]]. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* if (dropdown.isEditable()) console.log('Editing is enabled')
* ```
* @returns Whether or not this dropdown is editable.
*/
isEditable(): boolean {
return this.acroField.hasFlag(AcroChoiceFlags.Edit);
}
/**
* Allow users to edit the selected value of this dropdown in PDF readers
* with their keyboard. This means that the selected value of this dropdown
* will not be constrained by the list of available options. However, if this
* dropdown has any available options, users will still be allowed to select
* from that list.
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.enableEditing()
* ```
*/
enableEditing() {
this.acroField.setFlagTo(AcroChoiceFlags.Edit, true);
}
/**
* Do not allow users to edit the selected value of this dropdown in PDF
* readers with their keyboard. This will constrain the selected value of
* this dropdown to the list of available options. Users will only be able
* to select an option from that list.
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.disableEditing()
* ```
*/
disableEditing() {
this.acroField.setFlagTo(AcroChoiceFlags.Edit, false);
}
/**
* Returns `true` if the option list of this dropdown is always displayed
* in alphabetical order, irrespective of the order in which the options
* were added to the dropdown. See [[PDFDropdown.enableSorting]] and
* [[PDFDropdown.disableSorting]]. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* if (dropdown.isSorted()) console.log('Sorting is enabled')
* ```
* @returns Whether or not this dropdown's options are sorted.
*/
isSorted(): boolean {
return this.acroField.hasFlag(AcroChoiceFlags.Sort);
}
/**
* Always display the option list of this dropdown in alphabetical order,
* irrespective of the order in which the options were added to this dropdown.
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.enableSorting()
* ```
*/
enableSorting() {
this.acroField.setFlagTo(AcroChoiceFlags.Sort, true);
}
/**
* Do not always display the option list of this dropdown in alphabetical
* order. Instead, display the options in whichever order they were added
* to the list. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.disableSorting()
* ```
*/
disableSorting() {
this.acroField.setFlagTo(AcroChoiceFlags.Sort, false);
}
/**
* Returns `true` if multiple options can be selected from this dropdown's
* option list. See [[PDFDropdown.enableMultiselect]] and
* [[PDFDropdown.disableMultiselect]]. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* if (dropdown.isMultiselect()) console.log('Multiselect is enabled')
* ```
* @returns Whether or not multiple options can be selected.
*/
isMultiselect(): boolean {
return this.acroField.hasFlag(AcroChoiceFlags.MultiSelect);
}
/**
* Allow users to select more than one option from this dropdown's option
* list. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.enableMultiselect()
* ```
*/
enableMultiselect() {
this.acroField.setFlagTo(AcroChoiceFlags.MultiSelect, true);
}
/**
* Do not allow users to select more than one option from this dropdown's
* option list. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.disableMultiselect()
* ```
*/
disableMultiselect() {
this.acroField.setFlagTo(AcroChoiceFlags.MultiSelect, false);
}
/**
* Returns `true` if the selected option should be spell checked by PDF
* readers. Spell checking will only be performed if this dropdown allows
* editing (see [[PDFDropdown.isEditable]]). See
* [[PDFDropdown.enableSpellChecking]] and
* [[PDFDropdown.disableSpellChecking]]. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* if (dropdown.isSpellChecked()) console.log('Spell checking is enabled')
* ```
* @returns Whether or not this dropdown can be spell checked.
*/
isSpellChecked(): boolean {
return !this.acroField.hasFlag(AcroChoiceFlags.DoNotSpellCheck);
}
/**
* Allow PDF readers to spell check the selected option of this dropdown.
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.enableSpellChecking()
* ```
*/
enableSpellChecking() {
this.acroField.setFlagTo(AcroChoiceFlags.DoNotSpellCheck, false);
}
/**
* Do not allow PDF readers to spell check the selected option of this
* dropdown. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.disableSpellChecking()
* ```
*/
disableSpellChecking() {
this.acroField.setFlagTo(AcroChoiceFlags.DoNotSpellCheck, true);
}
/**
* Returns `true` if the option selected by a user is stored, or "committed",
* when the user clicks the option. The alternative is that the user's
* selection is stored when the user leaves this dropdown field (by clicking
* outside of it - on another field, for example). See
* [[PDFDropdown.enableSelectOnClick]] and
* [[PDFDropdown.disableSelectOnClick]]. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* if (dropdown.isSelectOnClick()) console.log('Select on click is enabled')
* ```
* @returns Whether or not options are selected immediately after they are
* clicked.
*/
isSelectOnClick(): boolean {
return this.acroField.hasFlag(AcroChoiceFlags.CommitOnSelChange);
}
/**
* Store the option selected by a user immediately after the user clicks the
* option. Do not wait for the user to leave this dropdown field (by clicking
* outside of it - on another field, for example). For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.enableSelectOnClick()
* ```
*/
enableSelectOnClick() {
this.acroField.setFlagTo(AcroChoiceFlags.CommitOnSelChange, true);
}
/**
* Wait to store the option selected by a user until they leave this dropdown
* field (by clicking outside of it - on another field, for example).
* For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.disableSelectOnClick()
* ```
*/
disableSelectOnClick() {
this.acroField.setFlagTo(AcroChoiceFlags.CommitOnSelChange, false);
}
/**
* Show this dropdown on the specified page. For example:
* ```js
* const ubuntuFont = await pdfDoc.embedFont(ubuntuFontBytes)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const dropdown = form.createDropdown('best.gundam')
* dropdown.setOptions(['Exia', 'Dynames'])
* dropdown.select('Exia')
*
* dropdown.addToPage(page, {
* x: 50,
* y: 75,
* width: 200,
* height: 100,
* textColor: rgb(1, 0, 0),
* backgroundColor: rgb(0, 1, 0),
* borderColor: rgb(0, 0, 1),
* borderWidth: 2,
* rotate: degrees(90),
* font: ubuntuFont,
* })
* ```
* This will create a new widget for this dropdown field.
* @param page The page to which this dropdown widget should be added.
* @param options The options to be used when adding this dropdown widget.
*/
addToPage(page: PDFPage, options?: FieldAppearanceOptions) {
assertIs(page, 'page', [[PDFPage, 'PDFPage']]);
assertFieldAppearanceOptions(options);
if (!options) options = {};
if (!('textColor' in options)) options.textColor = rgb(0, 0, 0);
if (!('backgroundColor' in options)) options.backgroundColor = rgb(1, 1, 1);
if (!('borderColor' in options)) options.borderColor = rgb(0, 0, 0);
if (!('borderWidth' in options)) options.borderWidth = 1;
// Create a widget for this dropdown
const widget = this.createWidget({
x: options.x ?? 0,
y: options.y ?? 0,
width: options.width ?? 200,
height: options.height ?? 50,
textColor: options.textColor,
backgroundColor: options.backgroundColor,
borderColor: options.borderColor,
borderWidth: options.borderWidth ?? 0,
rotate: options.rotate ?? degrees(0),
hidden: options.hidden,
page: page.ref,
});
const widgetRef = this.doc.context.register(widget.dict);
// Add widget to this field
this.acroField.addWidget(widgetRef);
// Set appearance streams for widget
const font = options.font ?? this.doc.getForm().getDefaultFont();
this.updateWidgetAppearance(widget, font);
// Add widget to the given page
page.node.addAnnot(widgetRef);
}
/**
* Returns `true` if this dropdown has been marked as dirty, or if any of
* this dropdown's widgets do not have an appearance stream. For example:
* ```js
* const dropdown = form.getDropdown('some.dropdown.field')
* if (dropdown.needsAppearancesUpdate()) console.log('Needs update')
* ```
* @returns Whether or not this dropdown needs an appearance update.
*/
needsAppearancesUpdate(): boolean {
if (this.isDirty()) return true;
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const hasAppearances =
widget.getAppearances()?.normal instanceof PDFStream;
if (!hasAppearances) return true;
}
return false;
}
/**
* Update the appearance streams for each of this dropdown's widgets using
* the default appearance provider for dropdowns. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.defaultUpdateAppearances(helvetica)
* ```
* @param font The font to be used for creating the appearance streams.
*/
defaultUpdateAppearances(font: PDFFont) {
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
this.updateAppearances(font);
}
/**
* Update the appearance streams for each of this dropdown's widgets using
* the given appearance provider. If no `provider` is passed, the default
* appearance provider for dropdowns will be used. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const dropdown = form.getDropdown('some.dropdown.field')
* dropdown.updateAppearances(helvetica, (field, widget, font) => {
* ...
* return drawTextField(...)
* })
* ```
* @param font The font to be used for creating the appearance streams.
* @param provider Optionally, the appearance provider to be used for
* generating the contents of the appearance streams.
*/
updateAppearances(
font: PDFFont,
provider?: AppearanceProviderFor<PDFDropdown>,
) {
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
assertOrUndefined(provider, 'provider', [Function]);
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
this.updateWidgetAppearance(widget, font, provider);
}
this.markAsClean();
}
// getOption(index: number): string {}
// getSelectedIndices(): number[] {}
// removeOptions(option: string | string[]) {}
// removeIndices(option: number[]) {}
// deselect(options: string | string[]) {}
// deselectIndices(optionIndices: number[]) {}
private updateWidgetAppearance(
widget: PDFWidgetAnnotation,
font: PDFFont,
provider?: AppearanceProviderFor<PDFDropdown>,
) {
const apProvider = provider ?? defaultDropdownAppearanceProvider;
const appearances = normalizeAppearance(apProvider(this, widget, font));
this.updateWidgetAppearanceWithFont(widget, font, appearances);
}
}

521
node_modules/pdf-lib/src/api/form/PDFField.ts generated vendored Normal file
View File

@@ -0,0 +1,521 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFFont from 'src/api/PDFFont';
import { AppearanceMapping } from 'src/api/form/appearances';
import { Color, colorToComponents, setFillingColor } from 'src/api/colors';
import {
Rotation,
toDegrees,
rotateRectangle,
reduceRotation,
adjustDimsForRotation,
degrees,
} from 'src/api/rotations';
import {
PDFRef,
PDFWidgetAnnotation,
PDFOperator,
PDFName,
PDFDict,
MethodNotImplementedError,
AcroFieldFlags,
PDFAcroTerminal,
AnnotationFlags,
} from 'src/core';
import { assertIs, assertMultiple, assertOrUndefined } from 'src/utils';
import { ImageAlignment } from '../image';
import PDFImage from '../PDFImage';
import { drawImage, rotateInPlace } from '../operations';
export interface FieldAppearanceOptions {
x?: number;
y?: number;
width?: number;
height?: number;
textColor?: Color;
backgroundColor?: Color;
borderColor?: Color;
borderWidth?: number;
rotate?: Rotation;
font?: PDFFont;
hidden?: boolean;
}
export const assertFieldAppearanceOptions = (
options?: FieldAppearanceOptions,
) => {
assertOrUndefined(options?.x, 'options.x', ['number']);
assertOrUndefined(options?.y, 'options.y', ['number']);
assertOrUndefined(options?.width, 'options.width', ['number']);
assertOrUndefined(options?.height, 'options.height', ['number']);
assertOrUndefined(options?.textColor, 'options.textColor', [
[Object, 'Color'],
]);
assertOrUndefined(options?.backgroundColor, 'options.backgroundColor', [
[Object, 'Color'],
]);
assertOrUndefined(options?.borderColor, 'options.borderColor', [
[Object, 'Color'],
]);
assertOrUndefined(options?.borderWidth, 'options.borderWidth', ['number']);
assertOrUndefined(options?.rotate, 'options.rotate', [[Object, 'Rotation']]);
};
/**
* Represents a field of a [[PDFForm]].
*
* This class is effectively abstract. All fields in a [[PDFForm]] will
* actually be an instance of a subclass of this class.
*
* Note that each field in a PDF is represented by a single field object.
* However, a given field object may be rendered at multiple locations within
* the document (across one or more pages). The rendering of a field is
* controlled by its widgets. Each widget causes its field to be displayed at a
* particular location in the document.
*
* Most of the time each field in a PDF has only a single widget, and thus is
* only rendered once. However, if a field is rendered multiple times, it will
* have multiple widgets - one for each location it is rendered.
*
* This abstraction of field objects and widgets is defined in the PDF
* specification and dictates how PDF files store fields and where they are
* to be rendered.
*/
export default class PDFField {
/** The low-level PDFAcroTerminal wrapped by this field. */
readonly acroField: PDFAcroTerminal;
/** The unique reference assigned to this field within the document. */
readonly ref: PDFRef;
/** The document to which this field belongs. */
readonly doc: PDFDocument;
protected constructor(
acroField: PDFAcroTerminal,
ref: PDFRef,
doc: PDFDocument,
) {
assertIs(acroField, 'acroField', [[PDFAcroTerminal, 'PDFAcroTerminal']]);
assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]);
assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]);
this.acroField = acroField;
this.ref = ref;
this.doc = doc;
}
/**
* Get the fully qualified name of this field. For example:
* ```js
* const fields = form.getFields()
* fields.forEach(field => {
* const name = field.getName()
* console.log('Field name:', name)
* })
* ```
* Note that PDF fields are structured as a tree. Each field is the
* descendent of a series of ancestor nodes all the way up to the form node,
* which is always the root of the tree. Each node in the tree (except for
* the form node) has a partial name. Partial names can be composed of any
* unicode characters except a period (`.`). The fully qualified name of a
* field is composed of the partial names of all its ancestors joined
* with periods. This means that splitting the fully qualified name on
* periods and taking the last element of the resulting array will give you
* the partial name of a specific field.
* @returns The fully qualified name of this field.
*/
getName(): string {
return this.acroField.getFullyQualifiedName() ?? '';
}
/**
* Returns `true` if this field is read only. This means that PDF readers
* will not allow users to interact with the field or change its value. See
* [[PDFField.enableReadOnly]] and [[PDFField.disableReadOnly]].
* For example:
* ```js
* const field = form.getField('some.field')
* if (field.isReadOnly()) console.log('Read only is enabled')
* ```
* @returns Whether or not this is a read only field.
*/
isReadOnly(): boolean {
return this.acroField.hasFlag(AcroFieldFlags.ReadOnly);
}
/**
* Prevent PDF readers from allowing users to interact with this field or
* change its value. The field will not respond to mouse or keyboard input.
* For example:
* ```js
* const field = form.getField('some.field')
* field.enableReadOnly()
* ```
* Useful for fields whose values are computed, imported from a database, or
* prefilled by software before being displayed to the user.
*/
enableReadOnly() {
this.acroField.setFlagTo(AcroFieldFlags.ReadOnly, true);
}
/**
* Allow users to interact with this field and change its value in PDF
* readers via mouse and keyboard input. For example:
* ```js
* const field = form.getField('some.field')
* field.disableReadOnly()
* ```
*/
disableReadOnly() {
this.acroField.setFlagTo(AcroFieldFlags.ReadOnly, false);
}
/**
* Returns `true` if this field must have a value when the form is submitted.
* See [[PDFField.enableRequired]] and [[PDFField.disableRequired]].
* For example:
* ```js
* const field = form.getField('some.field')
* if (field.isRequired()) console.log('Field is required')
* ```
* @returns Whether or not this field is required.
*/
isRequired(): boolean {
return this.acroField.hasFlag(AcroFieldFlags.Required);
}
/**
* Require this field to have a value when the form is submitted.
* For example:
* ```js
* const field = form.getField('some.field')
* field.enableRequired()
* ```
*/
enableRequired() {
this.acroField.setFlagTo(AcroFieldFlags.Required, true);
}
/**
* Do not require this field to have a value when the form is submitted.
* For example:
* ```js
* const field = form.getField('some.field')
* field.disableRequired()
* ```
*/
disableRequired() {
this.acroField.setFlagTo(AcroFieldFlags.Required, false);
}
/**
* Returns `true` if this field's value should be exported when the form is
* submitted. See [[PDFField.enableExporting]] and
* [[PDFField.disableExporting]].
* For example:
* ```js
* const field = form.getField('some.field')
* if (field.isExported()) console.log('Exporting is enabled')
* ```
* @returns Whether or not this field's value should be exported.
*/
isExported(): boolean {
return !this.acroField.hasFlag(AcroFieldFlags.NoExport);
}
/**
* Indicate that this field's value should be exported when the form is
* submitted in a PDF reader. For example:
* ```js
* const field = form.getField('some.field')
* field.enableExporting()
* ```
*/
enableExporting() {
this.acroField.setFlagTo(AcroFieldFlags.NoExport, false);
}
/**
* Indicate that this field's value should **not** be exported when the form
* is submitted in a PDF reader. For example:
* ```js
* const field = form.getField('some.field')
* field.disableExporting()
* ```
*/
disableExporting() {
this.acroField.setFlagTo(AcroFieldFlags.NoExport, true);
}
/** @ignore */
needsAppearancesUpdate(): boolean {
throw new MethodNotImplementedError(
this.constructor.name,
'needsAppearancesUpdate',
);
}
/** @ignore */
defaultUpdateAppearances(_font: PDFFont) {
throw new MethodNotImplementedError(
this.constructor.name,
'defaultUpdateAppearances',
);
}
protected markAsDirty() {
this.doc.getForm().markFieldAsDirty(this.ref);
}
protected markAsClean() {
this.doc.getForm().markFieldAsClean(this.ref);
}
protected isDirty(): boolean {
return this.doc.getForm().fieldIsDirty(this.ref);
}
protected createWidget(options: {
x: number;
y: number;
width: number;
height: number;
textColor?: Color;
backgroundColor?: Color;
borderColor?: Color;
borderWidth: number;
rotate: Rotation;
caption?: string;
hidden?: boolean;
page?: PDFRef;
}): PDFWidgetAnnotation {
const textColor = options.textColor;
const backgroundColor = options.backgroundColor;
const borderColor = options.borderColor;
const borderWidth = options.borderWidth;
const degreesAngle = toDegrees(options.rotate);
const caption = options.caption;
const x = options.x;
const y = options.y;
const width = options.width + borderWidth;
const height = options.height + borderWidth;
const hidden = Boolean(options.hidden);
const pageRef = options.page;
assertMultiple(degreesAngle, 'degreesAngle', 90);
// Create a widget for this field
const widget = PDFWidgetAnnotation.create(this.doc.context, this.ref);
// Set widget properties
const rect = rotateRectangle(
{ x, y, width, height },
borderWidth,
degreesAngle,
);
widget.setRectangle(rect);
if (pageRef) widget.setP(pageRef);
const ac = widget.getOrCreateAppearanceCharacteristics();
if (backgroundColor) {
ac.setBackgroundColor(colorToComponents(backgroundColor));
}
ac.setRotation(degreesAngle);
if (caption) ac.setCaptions({ normal: caption });
if (borderColor) ac.setBorderColor(colorToComponents(borderColor));
const bs = widget.getOrCreateBorderStyle();
if (borderWidth !== undefined) bs.setWidth(borderWidth);
widget.setFlagTo(AnnotationFlags.Print, true);
widget.setFlagTo(AnnotationFlags.Hidden, hidden);
widget.setFlagTo(AnnotationFlags.Invisible, false);
// Set acrofield properties
if (textColor) {
const da = this.acroField.getDefaultAppearance() ?? '';
const newDa = da + '\n' + setFillingColor(textColor).toString();
this.acroField.setDefaultAppearance(newDa);
}
return widget;
}
protected updateWidgetAppearanceWithFont(
widget: PDFWidgetAnnotation,
font: PDFFont,
{ normal, rollover, down }: AppearanceMapping<PDFOperator[]>,
) {
this.updateWidgetAppearances(widget, {
normal: this.createAppearanceStream(widget, normal, font),
rollover: rollover && this.createAppearanceStream(widget, rollover, font),
down: down && this.createAppearanceStream(widget, down, font),
});
}
protected updateOnOffWidgetAppearance(
widget: PDFWidgetAnnotation,
onValue: PDFName,
{
normal,
rollover,
down,
}: AppearanceMapping<{ on: PDFOperator[]; off: PDFOperator[] }>,
) {
this.updateWidgetAppearances(widget, {
normal: this.createAppearanceDict(widget, normal, onValue),
rollover:
rollover && this.createAppearanceDict(widget, rollover, onValue),
down: down && this.createAppearanceDict(widget, down, onValue),
});
}
protected updateWidgetAppearances(
widget: PDFWidgetAnnotation,
{ normal, rollover, down }: AppearanceMapping<PDFRef | PDFDict>,
) {
widget.setNormalAppearance(normal);
if (rollover) {
widget.setRolloverAppearance(rollover);
} else {
widget.removeRolloverAppearance();
}
if (down) {
widget.setDownAppearance(down);
} else {
widget.removeDownAppearance();
}
}
// // TODO: Do we need to do this...?
// private foo(font: PDFFont, dict: PDFDict) {
// if (!dict.lookup(PDFName.of('DR'))) {
// dict.set(PDFName.of('DR'), dict.context.obj({}));
// }
// const DR = dict.lookup(PDFName.of('DR'), PDFDict);
// if (!DR.lookup(PDFName.of('Font'))) {
// DR.set(PDFName.of('Font'), dict.context.obj({}));
// }
// const Font = DR.lookup(PDFName.of('Font'), PDFDict);
// Font.set(PDFName.of(font.name), font.ref);
// }
private createAppearanceStream(
widget: PDFWidgetAnnotation,
appearance: PDFOperator[],
font?: PDFFont,
): PDFRef {
const { context } = this.acroField.dict;
const { width, height } = widget.getRectangle();
// TODO: Do we need to do this...?
// if (font) {
// this.foo(font, widget.dict);
// this.foo(font, this.doc.getForm().acroForm.dict);
// }
// END TODO
const Resources = font && { Font: { [font.name]: font.ref } };
const stream = context.formXObject(appearance, {
Resources,
BBox: context.obj([0, 0, width, height]),
Matrix: context.obj([1, 0, 0, 1, 0, 0]),
});
const streamRef = context.register(stream);
return streamRef;
}
/**
* Create a FormXObject of the supplied image and add it to context.
* The FormXObject size is calculated based on the widget (including
* the alignment).
* @param widget The widget that should display the image.
* @param alignment The alignment of the image.
* @param image The image that should be displayed.
* @returns The ref for the FormXObject that was added to the context.
*/
protected createImageAppearanceStream(
widget: PDFWidgetAnnotation,
image: PDFImage,
alignment: ImageAlignment,
): PDFRef {
// NOTE: This implementation doesn't handle image borders.
// NOTE: Acrobat seems to resize the image (maybe even skewing its aspect
// ratio) to fit perfectly within the widget's rectangle. This method
// does not currently do that. Should there be an option for that?
const { context } = this.acroField.dict;
const rectangle = widget.getRectangle();
const ap = widget.getAppearanceCharacteristics();
const bs = widget.getBorderStyle();
const borderWidth = bs?.getWidth() ?? 0;
const rotation = reduceRotation(ap?.getRotation());
const rotate = rotateInPlace({ ...rectangle, rotation });
const adj = adjustDimsForRotation(rectangle, rotation);
const imageDims = image.scaleToFit(
adj.width - borderWidth * 2,
adj.height - borderWidth * 2,
);
// Support borders on images and maybe other properties
const options = {
x: borderWidth,
y: borderWidth,
width: imageDims.width,
height: imageDims.height,
//
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
};
if (alignment === ImageAlignment.Center) {
options.x += (adj.width - borderWidth * 2) / 2 - imageDims.width / 2;
options.y += (adj.height - borderWidth * 2) / 2 - imageDims.height / 2;
} else if (alignment === ImageAlignment.Right) {
options.x = adj.width - borderWidth - imageDims.width;
options.y = adj.height - borderWidth - imageDims.height;
}
const imageName = this.doc.context.addRandomSuffix('Image', 10);
const appearance = [...rotate, ...drawImage(imageName, options)];
////////////
const Resources = { XObject: { [imageName]: image.ref } };
const stream = context.formXObject(appearance, {
Resources,
BBox: context.obj([0, 0, rectangle.width, rectangle.height]),
Matrix: context.obj([1, 0, 0, 1, 0, 0]),
});
return context.register(stream);
}
private createAppearanceDict(
widget: PDFWidgetAnnotation,
appearance: { on: PDFOperator[]; off: PDFOperator[] },
onValue: PDFName,
): PDFDict {
const { context } = this.acroField.dict;
const onStreamRef = this.createAppearanceStream(widget, appearance.on);
const offStreamRef = this.createAppearanceStream(widget, appearance.off);
const appearanceDict = context.obj({});
appearanceDict.set(onValue, onStreamRef);
appearanceDict.set(PDFName.of('Off'), offStreamRef);
return appearanceDict;
}
}

852
node_modules/pdf-lib/src/api/form/PDFForm.ts generated vendored Normal file
View File

@@ -0,0 +1,852 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFPage from 'src/api/PDFPage';
import PDFField from 'src/api/form/PDFField';
import PDFButton from 'src/api/form/PDFButton';
import PDFCheckBox from 'src/api/form/PDFCheckBox';
import PDFDropdown from 'src/api/form/PDFDropdown';
import PDFOptionList from 'src/api/form/PDFOptionList';
import PDFRadioGroup from 'src/api/form/PDFRadioGroup';
import PDFSignature from 'src/api/form/PDFSignature';
import PDFTextField from 'src/api/form/PDFTextField';
import {
NoSuchFieldError,
UnexpectedFieldTypeError,
FieldAlreadyExistsError,
InvalidFieldNamePartError,
} from 'src/api/errors';
import PDFFont from 'src/api/PDFFont';
import { StandardFonts } from 'src/api/StandardFonts';
import { rotateInPlace } from 'src/api/operations';
import {
drawObject,
popGraphicsState,
pushGraphicsState,
translate,
} from 'src/api/operators';
import {
PDFAcroForm,
PDFAcroField,
PDFAcroCheckBox,
PDFAcroComboBox,
PDFAcroListBox,
PDFAcroRadioButton,
PDFAcroSignature,
PDFAcroText,
PDFAcroPushButton,
PDFAcroNonTerminal,
PDFDict,
PDFOperator,
PDFRef,
createPDFAcroFields,
PDFName,
PDFWidgetAnnotation,
} from 'src/core';
import { assertIs, Cache, assertOrUndefined } from 'src/utils';
export interface FlattenOptions {
updateFieldAppearances: boolean;
}
/**
* Represents the interactive form of a [[PDFDocument]].
*
* Interactive forms (sometimes called _AcroForms_) are collections of fields
* designed to gather information from a user. A PDF document may contains any
* number of fields that appear on various pages, all of which make up a single,
* global interactive form spanning the entire document. This means that
* instances of [[PDFDocument]] shall contain at most one [[PDFForm]].
*
* The fields of an interactive form are represented by [[PDFField]] instances.
*/
export default class PDFForm {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFDocument.getForm]] method, which will create an
* > instance of [[PDFForm]] for you.
*
* Create an instance of [[PDFForm]] from an existing acroForm and embedder
*
* @param acroForm The underlying `PDFAcroForm` for this form.
* @param doc The document to which the form will belong.
*/
static of = (acroForm: PDFAcroForm, doc: PDFDocument) =>
new PDFForm(acroForm, doc);
/** The low-level PDFAcroForm wrapped by this form. */
readonly acroForm: PDFAcroForm;
/** The document to which this form belongs. */
readonly doc: PDFDocument;
private readonly dirtyFields: Set<PDFRef>;
private readonly defaultFontCache: Cache<PDFFont>;
private constructor(acroForm: PDFAcroForm, doc: PDFDocument) {
assertIs(acroForm, 'acroForm', [[PDFAcroForm, 'PDFAcroForm']]);
assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]);
this.acroForm = acroForm;
this.doc = doc;
this.dirtyFields = new Set();
this.defaultFontCache = Cache.populatedBy(this.embedDefaultFont);
}
/**
* Returns `true` if this [[PDFForm]] has XFA data. Most PDFs with form
* fields do not use XFA as it is not widely supported by PDF readers.
*
* > `pdf-lib` does not support creation, modification, or reading of XFA
* > fields.
*
* For example:
* ```js
* const form = pdfDoc.getForm()
* if (form.hasXFA()) console.log('PDF has XFA data')
* ```
* @returns Whether or not this form has XFA data.
*/
hasXFA(): boolean {
return this.acroForm.dict.has(PDFName.of('XFA'));
}
/**
* Disconnect the XFA data from this [[PDFForm]] (if any exists). This will
* force readers to fallback to standard fields if the [[PDFDocument]]
* contains any. For example:
*
* For example:
* ```js
* const form = pdfDoc.getForm()
* form.deleteXFA()
* ```
*/
deleteXFA(): void {
this.acroForm.dict.delete(PDFName.of('XFA'));
}
/**
* Get all fields contained in this [[PDFForm]]. For example:
* ```js
* const form = pdfDoc.getForm()
* const fields = form.getFields()
* fields.forEach(field => {
* const type = field.constructor.name
* const name = field.getName()
* console.log(`${type}: ${name}`)
* })
* ```
* @returns An array of all fields in this form.
*/
getFields(): PDFField[] {
const allFields = this.acroForm.getAllFields();
const fields: PDFField[] = [];
for (let idx = 0, len = allFields.length; idx < len; idx++) {
const [acroField, ref] = allFields[idx];
const field = convertToPDFField(acroField, ref, this.doc);
if (field) fields.push(field);
}
return fields;
}
/**
* Get the field in this [[PDFForm]] with the given name. For example:
* ```js
* const form = pdfDoc.getForm()
* const field = form.getFieldMaybe('Page1.Foo.Bar[0]')
* if (field) console.log('Field exists!')
* ```
* @param name A fully qualified field name.
* @returns The field with the specified name, if one exists.
*/
getFieldMaybe(name: string): PDFField | undefined {
assertIs(name, 'name', ['string']);
const fields = this.getFields();
for (let idx = 0, len = fields.length; idx < len; idx++) {
const field = fields[idx];
if (field.getName() === name) return field;
}
return undefined;
}
/**
* Get the field in this [[PDFForm]] with the given name. For example:
* ```js
* const form = pdfDoc.getForm()
* const field = form.getField('Page1.Foo.Bar[0]')
* ```
* If no field exists with the provided name, an error will be thrown.
* @param name A fully qualified field name.
* @returns The field with the specified name.
*/
getField(name: string): PDFField {
assertIs(name, 'name', ['string']);
const field = this.getFieldMaybe(name);
if (field) return field;
throw new NoSuchFieldError(name);
}
/**
* Get the button field in this [[PDFForm]] with the given name. For example:
* ```js
* const form = pdfDoc.getForm()
* const button = form.getButton('Page1.Foo.Button[0]')
* ```
* An error will be thrown if no field exists with the provided name, or if
* the field exists but is not a button.
* @param name A fully qualified button name.
* @returns The button with the specified name.
*/
getButton(name: string): PDFButton {
assertIs(name, 'name', ['string']);
const field = this.getField(name);
if (field instanceof PDFButton) return field;
throw new UnexpectedFieldTypeError(name, PDFButton, field);
}
/**
* Get the check box field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const form = pdfDoc.getForm()
* const checkBox = form.getCheckBox('Page1.Foo.CheckBox[0]')
* checkBox.check()
* ```
* An error will be thrown if no field exists with the provided name, or if
* the field exists but is not a check box.
* @param name A fully qualified check box name.
* @returns The check box with the specified name.
*/
getCheckBox(name: string): PDFCheckBox {
assertIs(name, 'name', ['string']);
const field = this.getField(name);
if (field instanceof PDFCheckBox) return field;
throw new UnexpectedFieldTypeError(name, PDFCheckBox, field);
}
/**
* Get the dropdown field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const form = pdfDoc.getForm()
* const dropdown = form.getDropdown('Page1.Foo.Dropdown[0]')
* const options = dropdown.getOptions()
* dropdown.select(options[0])
* ```
* An error will be thrown if no field exists with the provided name, or if
* the field exists but is not a dropdown.
* @param name A fully qualified dropdown name.
* @returns The dropdown with the specified name.
*/
getDropdown(name: string): PDFDropdown {
assertIs(name, 'name', ['string']);
const field = this.getField(name);
if (field instanceof PDFDropdown) return field;
throw new UnexpectedFieldTypeError(name, PDFDropdown, field);
}
/**
* Get the option list field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const form = pdfDoc.getForm()
* const optionList = form.getOptionList('Page1.Foo.OptionList[0]')
* const options = optionList.getOptions()
* optionList.select(options[0])
* ```
* An error will be thrown if no field exists with the provided name, or if
* the field exists but is not an option list.
* @param name A fully qualified option list name.
* @returns The option list with the specified name.
*/
getOptionList(name: string): PDFOptionList {
assertIs(name, 'name', ['string']);
const field = this.getField(name);
if (field instanceof PDFOptionList) return field;
throw new UnexpectedFieldTypeError(name, PDFOptionList, field);
}
/**
* Get the radio group field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const form = pdfDoc.getForm()
* const radioGroup = form.getRadioGroup('Page1.Foo.RadioGroup[0]')
* const options = radioGroup.getOptions()
* radioGroup.select(options[0])
* ```
* An error will be thrown if no field exists with the provided name, or if
* the field exists but is not a radio group.
* @param name A fully qualified radio group name.
* @returns The radio group with the specified name.
*/
getRadioGroup(name: string): PDFRadioGroup {
assertIs(name, 'name', ['string']);
const field = this.getField(name);
if (field instanceof PDFRadioGroup) return field;
throw new UnexpectedFieldTypeError(name, PDFRadioGroup, field);
}
/**
* Get the signature field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const form = pdfDoc.getForm()
* const signature = form.getSignature('Page1.Foo.Signature[0]')
* ```
* An error will be thrown if no field exists with the provided name, or if
* the field exists but is not a signature.
* @param name A fully qualified signature name.
* @returns The signature with the specified name.
*/
getSignature(name: string): PDFSignature {
assertIs(name, 'name', ['string']);
const field = this.getField(name);
if (field instanceof PDFSignature) return field;
throw new UnexpectedFieldTypeError(name, PDFSignature, field);
}
/**
* Get the text field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const form = pdfDoc.getForm()
* const textField = form.getTextField('Page1.Foo.TextField[0]')
* textField.setText('Are you designed to act or to be acted upon?')
* ```
* An error will be thrown if no field exists with the provided name, or if
* the field exists but is not a text field.
* @param name A fully qualified text field name.
* @returns The text field with the specified name.
*/
getTextField(name: string): PDFTextField {
assertIs(name, 'name', ['string']);
const field = this.getField(name);
if (field instanceof PDFTextField) return field;
throw new UnexpectedFieldTypeError(name, PDFTextField, field);
}
/**
* Create a new button field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const button = form.createButton('cool.new.button')
*
* button.addToPage('Do Stuff', font, page)
* ```
* An error will be thrown if a field already exists with the provided name.
* @param name The fully qualified name for the new button.
* @returns The new button field.
*/
createButton(name: string): PDFButton {
assertIs(name, 'name', ['string']);
const nameParts = splitFieldName(name);
const parent = this.findOrCreateNonTerminals(nameParts.nonTerminal);
const button = PDFAcroPushButton.create(this.doc.context);
button.setPartialName(nameParts.terminal);
addFieldToParent(parent, [button, button.ref], nameParts.terminal);
return PDFButton.of(button, button.ref, this.doc);
}
/**
* Create a new check box field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const checkBox = form.createCheckBox('cool.new.checkBox')
*
* checkBox.addToPage(page)
* ```
* An error will be thrown if a field already exists with the provided name.
* @param name The fully qualified name for the new check box.
* @returns The new check box field.
*/
createCheckBox(name: string): PDFCheckBox {
assertIs(name, 'name', ['string']);
const nameParts = splitFieldName(name);
const parent = this.findOrCreateNonTerminals(nameParts.nonTerminal);
const checkBox = PDFAcroCheckBox.create(this.doc.context);
checkBox.setPartialName(nameParts.terminal);
addFieldToParent(parent, [checkBox, checkBox.ref], nameParts.terminal);
return PDFCheckBox.of(checkBox, checkBox.ref, this.doc);
}
/**
* Create a new dropdown field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const dropdown = form.createDropdown('cool.new.dropdown')
*
* dropdown.addToPage(font, page)
* ```
* An error will be thrown if a field already exists with the provided name.
* @param name The fully qualified name for the new dropdown.
* @returns The new dropdown field.
*/
createDropdown(name: string): PDFDropdown {
assertIs(name, 'name', ['string']);
const nameParts = splitFieldName(name);
const parent = this.findOrCreateNonTerminals(nameParts.nonTerminal);
const comboBox = PDFAcroComboBox.create(this.doc.context);
comboBox.setPartialName(nameParts.terminal);
addFieldToParent(parent, [comboBox, comboBox.ref], nameParts.terminal);
return PDFDropdown.of(comboBox, comboBox.ref, this.doc);
}
/**
* Create a new option list field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const optionList = form.createOptionList('cool.new.optionList')
*
* optionList.addToPage(font, page)
* ```
* An error will be thrown if a field already exists with the provided name.
* @param name The fully qualified name for the new option list.
* @returns The new option list field.
*/
createOptionList(name: string): PDFOptionList {
assertIs(name, 'name', ['string']);
const nameParts = splitFieldName(name);
const parent = this.findOrCreateNonTerminals(nameParts.nonTerminal);
const listBox = PDFAcroListBox.create(this.doc.context);
listBox.setPartialName(nameParts.terminal);
addFieldToParent(parent, [listBox, listBox.ref], nameParts.terminal);
return PDFOptionList.of(listBox, listBox.ref, this.doc);
}
/**
* Create a new radio group field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const radioGroup = form.createRadioGroup('cool.new.radioGroup')
*
* radioGroup.addOptionToPage('is-dog', page, { y: 0 })
* radioGroup.addOptionToPage('is-cat', page, { y: 75 })
* ```
* An error will be thrown if a field already exists with the provided name.
* @param name The fully qualified name for the new radio group.
* @returns The new radio group field.
*/
createRadioGroup(name: string): PDFRadioGroup {
assertIs(name, 'name', ['string']);
const nameParts = splitFieldName(name);
const parent = this.findOrCreateNonTerminals(nameParts.nonTerminal);
const radioButton = PDFAcroRadioButton.create(this.doc.context);
radioButton.setPartialName(nameParts.terminal);
addFieldToParent(
parent,
[radioButton, radioButton.ref],
nameParts.terminal,
);
return PDFRadioGroup.of(radioButton, radioButton.ref, this.doc);
}
/**
* Create a new text field in this [[PDFForm]] with the given name.
* For example:
* ```js
* const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const textField = form.createTextField('cool.new.textField')
*
* textField.addToPage(font, page)
* ```
* An error will be thrown if a field already exists with the provided name.
* @param name The fully qualified name for the new radio group.
* @returns The new radio group field.
*/
createTextField(name: string): PDFTextField {
assertIs(name, 'name', ['string']);
const nameParts = splitFieldName(name);
const parent = this.findOrCreateNonTerminals(nameParts.nonTerminal);
const text = PDFAcroText.create(this.doc.context);
text.setPartialName(nameParts.terminal);
addFieldToParent(parent, [text, text.ref], nameParts.terminal);
return PDFTextField.of(text, text.ref, this.doc);
}
/**
* Flatten all fields in this [[PDFForm]].
*
* Flattening a form field will take the current appearance for each of that
* field's widgets and make them part of their page's content stream. All form
* fields and annotations associated are then removed. Note that once a form
* has been flattened its fields can no longer be accessed or edited.
*
* This operation is often used after filling form fields to ensure a
* consistent appearance across different PDF readers and/or printers.
* Another common use case is to copy a template document with form fields
* into another document. In this scenario you would load the template
* document, fill its fields, flatten it, and then copy its pages into the
* recipient document - the filled fields will be copied over.
*
* For example:
* ```js
* const form = pdfDoc.getForm();
* form.flatten();
* ```
*/
flatten(options: FlattenOptions = { updateFieldAppearances: true }) {
if (options.updateFieldAppearances) {
this.updateFieldAppearances();
}
const fields = this.getFields();
for (let i = 0, lenFields = fields.length; i < lenFields; i++) {
const field = fields[i];
const widgets = field.acroField.getWidgets();
for (let j = 0, lenWidgets = widgets.length; j < lenWidgets; j++) {
const widget = widgets[j];
const page = this.findWidgetPage(widget);
const widgetRef = this.findWidgetAppearanceRef(field, widget);
const xObjectKey = page.node.newXObject('FlatWidget', widgetRef);
const rectangle = widget.getRectangle();
const operators = [
pushGraphicsState(),
translate(rectangle.x, rectangle.y),
...rotateInPlace({ ...rectangle, rotation: 0 }),
drawObject(xObjectKey),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
page.pushOperators(...operators);
}
this.removeField(field);
}
}
/**
* Remove a field from this [[PDFForm]].
*
* For example:
* ```js
* const form = pdfDoc.getForm();
* const ageField = form.getFields().find(x => x.getName() === 'Age');
* form.removeField(ageField);
* ```
*/
removeField(field: PDFField) {
const widgets = field.acroField.getWidgets();
const pages: Set<PDFPage> = new Set();
for (let i = 0, len = widgets.length; i < len; i++) {
const widget = widgets[i];
const widgetRef = this.findWidgetAppearanceRef(field, widget);
const page = this.findWidgetPage(widget);
pages.add(page);
page.node.removeAnnot(widgetRef);
}
pages.forEach((page) => page.node.removeAnnot(field.ref));
this.acroForm.removeField(field.acroField);
const fieldKids = field.acroField.normalizedEntries().Kids;
const kidsCount = fieldKids.size();
for (let childIndex = 0; childIndex < kidsCount; childIndex++) {
const child = fieldKids.get(childIndex);
if (child instanceof PDFRef) {
this.doc.context.delete(child);
}
}
this.doc.context.delete(field.ref);
}
/**
* Update the appearance streams for all widgets of all fields in this
* [[PDFForm]]. Appearance streams will only be created for a widget if it
* does not have any existing appearance streams, or the field's value has
* changed (e.g. by calling [[PDFTextField.setText]] or
* [[PDFDropdown.select]]).
*
* For example:
* ```js
* const courier = await pdfDoc.embedFont(StandardFonts.Courier)
* const form = pdfDoc.getForm()
* form.updateFieldAppearances(courier)
* ```
*
* **IMPORTANT:** The default value for the `font` parameter is
* [[StandardFonts.Helvetica]]. Note that this is a WinAnsi font. This means
* that encoding errors will be thrown if any fields contain text with
* characters outside the WinAnsi character set (the latin alphabet).
*
* Embedding a custom font and passing that as the `font`
* parameter allows you to generate appearance streams with non WinAnsi
* characters (assuming your custom font supports them).
*
* > **NOTE:** The [[PDFDocument.save]] method will call this method to
* > update appearances automatically if a form was accessed via the
* > [[PDFDocument.getForm]] method prior to saving.
*
* @param font Optionally, the font to use when creating new appearances.
*/
updateFieldAppearances(font?: PDFFont) {
assertOrUndefined(font, 'font', [[PDFFont, 'PDFFont']]);
font = font ?? this.getDefaultFont();
const fields = this.getFields();
for (let idx = 0, len = fields.length; idx < len; idx++) {
const field = fields[idx];
if (field.needsAppearancesUpdate()) {
field.defaultUpdateAppearances(font);
}
}
}
/**
* Mark a field as dirty. This will cause its appearance streams to be
* updated by [[PDFForm.updateFieldAppearances]].
* ```js
* const form = pdfDoc.getForm()
* const field = form.getField('foo.bar')
* form.markFieldAsDirty(field.ref)
* ```
* @param fieldRef The reference to the field that should be marked.
*/
markFieldAsDirty(fieldRef: PDFRef) {
assertOrUndefined(fieldRef, 'fieldRef', [[PDFRef, 'PDFRef']]);
this.dirtyFields.add(fieldRef);
}
/**
* Mark a field as dirty. This will cause its appearance streams to not be
* updated by [[PDFForm.updateFieldAppearances]].
* ```js
* const form = pdfDoc.getForm()
* const field = form.getField('foo.bar')
* form.markFieldAsClean(field.ref)
* ```
* @param fieldRef The reference to the field that should be marked.
*/
markFieldAsClean(fieldRef: PDFRef) {
assertOrUndefined(fieldRef, 'fieldRef', [[PDFRef, 'PDFRef']]);
this.dirtyFields.delete(fieldRef);
}
/**
* Returns `true` is the specified field has been marked as dirty.
* ```js
* const form = pdfDoc.getForm()
* const field = form.getField('foo.bar')
* if (form.fieldIsDirty(field.ref)) console.log('Field is dirty')
* ```
* @param fieldRef The reference to the field that should be checked.
* @returns Whether or not the specified field is dirty.
*/
fieldIsDirty(fieldRef: PDFRef): boolean {
assertOrUndefined(fieldRef, 'fieldRef', [[PDFRef, 'PDFRef']]);
return this.dirtyFields.has(fieldRef);
}
getDefaultFont() {
return this.defaultFontCache.access();
}
private findWidgetPage(widget: PDFWidgetAnnotation): PDFPage {
const pageRef = widget.P();
let page = this.doc.getPages().find((x) => x.ref === pageRef);
if (page === undefined) {
const widgetRef = this.doc.context.getObjectRef(widget.dict);
if (widgetRef === undefined) {
throw new Error('Could not find PDFRef for PDFObject');
}
page = this.doc.findPageForAnnotationRef(widgetRef);
if (page === undefined) {
throw new Error(`Could not find page for PDFRef ${widgetRef}`);
}
}
return page;
}
private findWidgetAppearanceRef(
field: PDFField,
widget: PDFWidgetAnnotation,
): PDFRef {
let refOrDict = widget.getNormalAppearance();
if (
refOrDict instanceof PDFDict &&
(field instanceof PDFCheckBox || field instanceof PDFRadioGroup)
) {
const value = field.acroField.getValue();
const ref = refOrDict.get(value) ?? refOrDict.get(PDFName.of('Off'));
if (ref instanceof PDFRef) {
refOrDict = ref;
}
}
if (!(refOrDict instanceof PDFRef)) {
const name = field.getName();
throw new Error(`Failed to extract appearance ref for: ${name}`);
}
return refOrDict;
}
private findOrCreateNonTerminals(partialNames: string[]) {
let nonTerminal: [PDFAcroForm] | [PDFAcroNonTerminal, PDFRef] = [
this.acroForm,
];
for (let idx = 0, len = partialNames.length; idx < len; idx++) {
const namePart = partialNames[idx];
if (!namePart) throw new InvalidFieldNamePartError(namePart);
const [parent, parentRef] = nonTerminal;
const res = this.findNonTerminal(namePart, parent);
if (res) {
nonTerminal = res;
} else {
const node = PDFAcroNonTerminal.create(this.doc.context);
node.setPartialName(namePart);
node.setParent(parentRef);
const nodeRef = this.doc.context.register(node.dict);
parent.addField(nodeRef);
nonTerminal = [node, nodeRef];
}
}
return nonTerminal;
}
private findNonTerminal(
partialName: string,
parent: PDFAcroForm | PDFAcroNonTerminal,
): [PDFAcroNonTerminal, PDFRef] | undefined {
const fields =
parent instanceof PDFAcroForm
? this.acroForm.getFields()
: createPDFAcroFields(parent.Kids());
for (let idx = 0, len = fields.length; idx < len; idx++) {
const [field, ref] = fields[idx];
if (field.getPartialName() === partialName) {
if (field instanceof PDFAcroNonTerminal) return [field, ref];
throw new FieldAlreadyExistsError(partialName);
}
}
return undefined;
}
private embedDefaultFont = (): PDFFont =>
this.doc.embedStandardFont(StandardFonts.Helvetica);
}
const convertToPDFField = (
field: PDFAcroField,
ref: PDFRef,
doc: PDFDocument,
): PDFField | undefined => {
if (field instanceof PDFAcroPushButton) return PDFButton.of(field, ref, doc);
if (field instanceof PDFAcroCheckBox) return PDFCheckBox.of(field, ref, doc);
if (field instanceof PDFAcroComboBox) return PDFDropdown.of(field, ref, doc);
if (field instanceof PDFAcroListBox) return PDFOptionList.of(field, ref, doc);
if (field instanceof PDFAcroText) return PDFTextField.of(field, ref, doc);
if (field instanceof PDFAcroRadioButton) {
return PDFRadioGroup.of(field, ref, doc);
}
if (field instanceof PDFAcroSignature) {
return PDFSignature.of(field, ref, doc);
}
return undefined;
};
const splitFieldName = (fullyQualifiedName: string) => {
if (fullyQualifiedName.length === 0) {
throw new Error('PDF field names must not be empty strings');
}
const parts = fullyQualifiedName.split('.');
for (let idx = 0, len = parts.length; idx < len; idx++) {
if (parts[idx] === '') {
throw new Error(
`Periods in PDF field names must be separated by at least one character: "${fullyQualifiedName}"`,
);
}
}
if (parts.length === 1) return { nonTerminal: [], terminal: parts[0] };
return {
nonTerminal: parts.slice(0, parts.length - 1),
terminal: parts[parts.length - 1],
};
};
const addFieldToParent = (
[parent, parentRef]: [PDFAcroForm] | [PDFAcroNonTerminal, PDFRef],
[field, fieldRef]: [PDFAcroField, PDFRef],
partialName: string,
) => {
const entries = parent.normalizedEntries();
const fields = createPDFAcroFields(
'Kids' in entries ? entries.Kids : entries.Fields,
);
for (let idx = 0, len = fields.length; idx < len; idx++) {
if (fields[idx][0].getPartialName() === partialName) {
throw new FieldAlreadyExistsError(partialName);
}
}
parent.addField(fieldRef);
field.setParent(parentRef);
};

571
node_modules/pdf-lib/src/api/form/PDFOptionList.ts generated vendored Normal file
View File

@@ -0,0 +1,571 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFPage from 'src/api/PDFPage';
import PDFFont from 'src/api/PDFFont';
import PDFField, {
FieldAppearanceOptions,
assertFieldAppearanceOptions,
} from 'src/api/form/PDFField';
import {
AppearanceProviderFor,
normalizeAppearance,
defaultOptionListAppearanceProvider,
} from 'src/api/form/appearances';
import { rgb } from 'src/api/colors';
import { degrees } from 'src/api/rotations';
import {
PDFRef,
PDFHexString,
PDFString,
PDFStream,
PDFAcroListBox,
AcroChoiceFlags,
PDFWidgetAnnotation,
} from 'src/core';
import {
assertIs,
assertIsSubset,
assertOrUndefined,
assertPositive,
} from 'src/utils';
/**
* Represents an option list field of a [[PDFForm]].
*
* [[PDFOptionList]] fields are interactive lists of options. The purpose of an
* option list is to enable users to select one or more options from a set of
* possible options. Users are able to see the full set of options without
* first having to click on the field (though scrolling may be necessary).
* Clicking an option in the list will cause it to be selected and displayed
* with a highlighted background. Some option lists allow users to select
* more than one option (see [[PDFOptionList.isMultiselect]]).
*/
export default class PDFOptionList extends PDFField {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFForm.getOptionList]] method, which will create
* > an instance of [[PDFOptionList]] for you.
*
* Create an instance of [[PDFOptionList]] from an existing acroListBox and
* ref
*
* @param acroComboBox The underlying `PDFAcroListBox` for this option list.
* @param ref The unique reference for this option list.
* @param doc The document to which this option list will belong.
*/
static of = (acroListBox: PDFAcroListBox, ref: PDFRef, doc: PDFDocument) =>
new PDFOptionList(acroListBox, ref, doc);
/** The low-level PDFAcroListBox wrapped by this option list. */
readonly acroField: PDFAcroListBox;
private constructor(
acroListBox: PDFAcroListBox,
ref: PDFRef,
doc: PDFDocument,
) {
super(acroListBox, ref, doc);
assertIs(acroListBox, 'acroListBox', [[PDFAcroListBox, 'PDFAcroListBox']]);
this.acroField = acroListBox;
}
/**
* Get the list of available options for this option list. These options will
* be displayed to users who view this option list in a PDF reader.
* For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* const options = optionList.getOptions()
* console.log('Option List options:', options)
* ```
* @returns The options for this option list.
*/
getOptions(): string[] {
const rawOptions = this.acroField.getOptions();
const options = new Array<string>(rawOptions.length);
for (let idx = 0, len = options.length; idx < len; idx++) {
const { display, value } = rawOptions[idx];
options[idx] = (display ?? value).decodeText();
}
return options;
}
/**
* Get the selected options for this option list. These are the values that
* were selected by a human user via a PDF reader, or programatically via
* software.
* For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* const selections = optionList.getSelected()
* console.log('Option List selections:', selections)
* ```
* @returns The selected options for this option list.
*/
getSelected(): string[] {
const values = this.acroField.getValues();
const selected = new Array<string>(values.length);
for (let idx = 0, len = values.length; idx < len; idx++) {
selected[idx] = values[idx].decodeText();
}
return selected;
}
/**
* Set the list of options that are available for this option list. These are
* the values that will be available for users to select when they view this
* option list in a PDF reader. Note that preexisting options for this
* option list will be removed. Only the values passed as `options` will be
* available to select.
*
* For example:
* ```js
* const optionList = form.getOptionList('planets.optionList')
* optionList.setOptions(['Earth', 'Mars', 'Pluto', 'Venus'])
* ```
*
* This method will mark this option list as dirty, causing its appearance
* streams to be updated when either [[PDFDocument.save]] or
* [[PDFForm.updateFieldAppearances]] is called. The updated streams will
* display the options this field contains inside the widgets of this text
* field (with selected options highlighted).
*
* **IMPORTANT:** The default font used to update appearance streams is
* [[StandardFonts.Helvetica]]. Note that this is a WinAnsi font. This means
* that encoding errors will be thrown if this field contains any options
* with characters outside the WinAnsi character set (the latin alphabet).
*
* Embedding a custom font and passing it to
* [[PDFForm.updateFieldAppearances]] or [[PDFOptionList.updateAppearances]]
* allows you to generate appearance streams with characters outside the
* latin alphabet (assuming the custom font supports them).
*
* @param options The options that should be available in this option list.
*/
setOptions(options: string[]) {
assertIs(options, 'options', [Array]);
this.markAsDirty();
const optionObjects = new Array<{ value: PDFHexString }>(options.length);
for (let idx = 0, len = options.length; idx < len; idx++) {
optionObjects[idx] = { value: PDFHexString.fromText(options[idx]) };
}
this.acroField.setOptions(optionObjects);
}
/**
* Add to the list of options that are available for this option list. Users
* will be able to select these values in a PDF reader. In addition to the
* values passed as `options`, any preexisting options for this option list
* will still be available for users to select.
* For example:
* ```js
* const optionList = form.getOptionList('rockets.optionList')
* optionList.addOptions(['Saturn IV', 'Falcon Heavy'])
* ```
* This method will mark this option list as dirty. See
* [[PDFOptionList.setOptions]] for more details about what this means.
* @param options New options that should be available in this option list.
*/
addOptions(options: string | string[]) {
assertIs(options, 'options', ['string', Array]);
this.markAsDirty();
const optionsArr = Array.isArray(options) ? options : [options];
const existingOptions: {
value: PDFString | PDFHexString;
display?: PDFString | PDFHexString;
}[] = this.acroField.getOptions();
const newOptions = new Array<{ value: PDFHexString }>(optionsArr.length);
for (let idx = 0, len = optionsArr.length; idx < len; idx++) {
newOptions[idx] = { value: PDFHexString.fromText(optionsArr[idx]) };
}
this.acroField.setOptions(existingOptions.concat(newOptions));
}
/**
* Select one or more values for this option list. This operation is analogous
* to a human user opening the option list in a PDF reader and clicking on one
* or more values to select them. This method will update the underlying state
* of the option list to indicate which values have been selected. PDF
* libraries and readers will be able to extract these values from the saved
* document and determine which values were selected.
* For example:
* ```js
* const optionList = form.getOptionList('best.superheroes.optionList')
* optionList.select(['One Punch Man', 'Iron Man'])
* ```
* This method will mark this option list as dirty. See
* [[PDFOptionList.setOptions]] for more details about what this means.
* @param options The options to be selected.
* @param merge Whether or not existing selections should be preserved.
*/
select(options: string | string[], merge = false) {
assertIs(options, 'options', ['string', Array]);
assertIs(merge, 'merge', ['boolean']);
const optionsArr = Array.isArray(options) ? options : [options];
const validOptions = this.getOptions();
assertIsSubset(optionsArr, 'option', validOptions);
this.markAsDirty();
if (optionsArr.length > 1 || (optionsArr.length === 1 && merge)) {
this.enableMultiselect();
}
const values = new Array<PDFHexString>(optionsArr.length);
for (let idx = 0, len = optionsArr.length; idx < len; idx++) {
values[idx] = PDFHexString.fromText(optionsArr[idx]);
}
if (merge) {
const existingValues = this.acroField.getValues();
this.acroField.setValues(existingValues.concat(values));
} else {
this.acroField.setValues(values);
}
}
/**
* Clear all selected values for this option list. This operation is
* equivalent to selecting an empty list. This method will update the
* underlying state of the option list to indicate that no values have been
* selected.
* For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.clear()
* ```
* This method will mark this option list as dirty. See
* [[PDFOptionList.setOptions]] for more details about what this means.
*/
clear() {
this.markAsDirty();
this.acroField.setValues([]);
}
/**
* Set the font size for the text in this field. There needs to be a
* default appearance string (DA) set with a font value specified
* for this to work. For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.setFontSize(4);
* ```
* @param fontSize The font size to set the font to.
*/
/**
* Set the font size for this field. Larger font sizes will result in larger
* text being displayed when PDF readers render this option list. Font sizes
* may be integer or floating point numbers. Supplying a negative font size
* will cause this method to throw an error.
*
* For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.setFontSize(4)
* optionList.setFontSize(15.7)
* ```
*
* > This method depends upon the existence of a default appearance
* > (`/DA`) string. If this field does not have a default appearance string,
* > or that string does not contain a font size (via the `Tf` operator),
* > then this method will throw an error.
*
* @param fontSize The font size to be used when rendering text in this field.
*/
setFontSize(fontSize: number) {
assertPositive(fontSize, 'fontSize');
this.acroField.setFontSize(fontSize);
this.markAsDirty();
}
/**
* Returns `true` if the options of this option list are always displayed
* in alphabetical order, irrespective of the order in which the options
* were added to the option list. See [[PDFOptionList.enableSorting]] and
* [[PDFOptionList.disableSorting]]. For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* if (optionList.isSorted()) console.log('Sorting is enabled')
* ```
* @returns Whether or not this option list is sorted.
*/
isSorted(): boolean {
return this.acroField.hasFlag(AcroChoiceFlags.Sort);
}
/**
* Always display the options of this option list in alphabetical order,
* irrespective of the order in which the options were added to this option
* list.
* For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.enableSorting()
* ```
*/
enableSorting() {
this.acroField.setFlagTo(AcroChoiceFlags.Sort, true);
}
/**
* Do not always display the options of this option list in alphabetical
* order. Instead, display the options in whichever order they were added
* to this option list. For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.disableSorting()
* ```
*/
disableSorting() {
this.acroField.setFlagTo(AcroChoiceFlags.Sort, false);
}
/**
* Returns `true` if multiple options can be selected from this option list.
* See [[PDFOptionList.enableMultiselect]] and
* [[PDFOptionList.disableMultiselect]]. For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* if (optionList.isMultiselect()) console.log('Multiselect is enabled')
* ```
* @returns Whether or not multiple options can be selected.
*/
isMultiselect(): boolean {
return this.acroField.hasFlag(AcroChoiceFlags.MultiSelect);
}
/**
* Allow users to select more than one option from this option list.
* For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.enableMultiselect()
* ```
*/
enableMultiselect() {
this.acroField.setFlagTo(AcroChoiceFlags.MultiSelect, true);
}
/**
* Do not allow users to select more than one option from this option list.
* For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.disableMultiselect()
* ```
*/
disableMultiselect() {
this.acroField.setFlagTo(AcroChoiceFlags.MultiSelect, false);
}
/**
* Returns `true` if the option selected by a user is stored, or "committed",
* when the user clicks the option. The alternative is that the user's
* selection is stored when the user leaves this option list field (by
* clicking outside of it - on another field, for example). See
* [[PDFOptionList.enableSelectOnClick]] and
* [[PDFOptionList.disableSelectOnClick]]. For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* if (optionList.isSelectOnClick()) console.log('Select on click is enabled')
* ```
* @returns Whether or not options are selected immediately after they are
* clicked.
*/
isSelectOnClick(): boolean {
return this.acroField.hasFlag(AcroChoiceFlags.CommitOnSelChange);
}
/**
* Store the option selected by a user immediately after the user clicks the
* option. Do not wait for the user to leave this option list field (by
* clicking outside of it - on another field, for example). For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.enableSelectOnClick()
* ```
*/
enableSelectOnClick() {
this.acroField.setFlagTo(AcroChoiceFlags.CommitOnSelChange, true);
}
/**
* Wait to store the option selected by a user until they leave this option
* list field (by clicking outside of it - on another field, for example).
* For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* optionList.disableSelectOnClick()
* ```
*/
disableSelectOnClick() {
this.acroField.setFlagTo(AcroChoiceFlags.CommitOnSelChange, false);
}
/**
* Show this option list on the specified page. For example:
* ```js
* const ubuntuFont = await pdfDoc.embedFont(ubuntuFontBytes)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const optionList = form.createOptionList('best.gundams')
* optionList.setOptions(['Exia', 'Dynames', 'Kyrios', 'Virtue'])
* optionList.select(['Exia', 'Virtue'])
*
* optionList.addToPage(page, {
* x: 50,
* y: 75,
* width: 200,
* height: 100,
* textColor: rgb(1, 0, 0),
* backgroundColor: rgb(0, 1, 0),
* borderColor: rgb(0, 0, 1),
* borderWidth: 2,
* rotate: degrees(90),
* font: ubuntuFont,
* })
* ```
* This will create a new widget for this option list field.
* @param page The page to which this option list widget should be added.
* @param options The options to be used when adding this option list widget.
*/
addToPage(page: PDFPage, options?: FieldAppearanceOptions) {
assertIs(page, 'page', [[PDFPage, 'PDFPage']]);
assertFieldAppearanceOptions(options);
if (!options) options = {};
if (!('textColor' in options)) options.textColor = rgb(0, 0, 0);
if (!('backgroundColor' in options)) options.backgroundColor = rgb(1, 1, 1);
if (!('borderColor' in options)) options.borderColor = rgb(0, 0, 0);
if (!('borderWidth' in options)) options.borderWidth = 1;
// Create a widget for this option list
const widget = this.createWidget({
x: options.x ?? 0,
y: options.y ?? 0,
width: options.width ?? 200,
height: options.height ?? 100,
textColor: options.textColor,
backgroundColor: options.backgroundColor,
borderColor: options.borderColor,
borderWidth: options.borderWidth ?? 0,
rotate: options.rotate ?? degrees(0),
hidden: options.hidden,
page: page.ref,
});
const widgetRef = this.doc.context.register(widget.dict);
// Add widget to this field
this.acroField.addWidget(widgetRef);
// Set appearance streams for widget
const font = options.font ?? this.doc.getForm().getDefaultFont();
this.updateWidgetAppearance(widget, font);
// Add widget to the given page
page.node.addAnnot(widgetRef);
}
/**
* Returns `true` if this option list has been marked as dirty, or if any of
* this option list's widgets do not have an appearance stream. For example:
* ```js
* const optionList = form.getOptionList('some.optionList.field')
* if (optionList.needsAppearancesUpdate()) console.log('Needs update')
* ```
* @returns Whether or not this option list needs an appearance update.
*/
needsAppearancesUpdate(): boolean {
if (this.isDirty()) return true;
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const hasAppearances =
widget.getAppearances()?.normal instanceof PDFStream;
if (!hasAppearances) return true;
}
return false;
}
/**
* Update the appearance streams for each of this option list's widgets using
* the default appearance provider for option lists. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const optionList = form.getOptionList('some.optionList.field')
* optionList.defaultUpdateAppearances(helvetica)
* ```
* @param font The font to be used for creating the appearance streams.
*/
defaultUpdateAppearances(font: PDFFont) {
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
this.updateAppearances(font);
}
/**
* Update the appearance streams for each of this option list's widgets using
* the given appearance provider. If no `provider` is passed, the default
* appearance provider for option lists will be used. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const optionList = form.getOptionList('some.optionList.field')
* optionList.updateAppearances(helvetica, (field, widget, font) => {
* ...
* return drawOptionList(...)
* })
* ```
* @param font The font to be used for creating the appearance streams.
* @param provider Optionally, the appearance provider to be used for
* generating the contents of the appearance streams.
*/
updateAppearances(
font: PDFFont,
provider?: AppearanceProviderFor<PDFOptionList>,
) {
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
assertOrUndefined(provider, 'provider', [Function]);
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
this.updateWidgetAppearance(widget, font, provider);
}
this.markAsClean();
}
// getOption(index: number): string {}
// getSelectedIndices(): number[] {}
// removeOptions(option: string | string[]) {}
// removeIndices(option: number[]) {}
// deselect(options: string | string[]) {}
// deselectIndices(optionIndices: number[]) {}
private updateWidgetAppearance(
widget: PDFWidgetAnnotation,
font: PDFFont,
provider?: AppearanceProviderFor<PDFOptionList>,
) {
const apProvider = provider ?? defaultOptionListAppearanceProvider;
const appearances = normalizeAppearance(apProvider(this, widget, font));
this.updateWidgetAppearanceWithFont(widget, font, appearances);
}
}

471
node_modules/pdf-lib/src/api/form/PDFRadioGroup.ts generated vendored Normal file
View File

@@ -0,0 +1,471 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFPage from 'src/api/PDFPage';
import PDFField, {
FieldAppearanceOptions,
assertFieldAppearanceOptions,
} from 'src/api/form/PDFField';
import {
AppearanceProviderFor,
normalizeAppearance,
defaultRadioGroupAppearanceProvider,
} from 'src/api/form/appearances';
import { rgb } from 'src/api/colors';
import { degrees } from 'src/api/rotations';
import {
PDFName,
PDFRef,
PDFHexString,
PDFDict,
PDFWidgetAnnotation,
PDFAcroRadioButton,
AcroButtonFlags,
} from 'src/core';
import { assertIs, assertOrUndefined, assertIsOneOf } from 'src/utils';
/**
* Represents a radio group field of a [[PDFForm]].
*
* [[PDFRadioGroup]] fields are collections of radio buttons. The purpose of a
* radio group is to enable users to select one option from a set of mutually
* exclusive choices. Each choice in a radio group is represented by a radio
* button. Radio buttons each have two states: `on` and `off`. At most one
* radio button in a group may be in the `on` state at any time. Users can
* click on a radio button to select it (and thereby automatically deselect any
* other radio button that might have already been selected). Some radio
* groups allow users to toggle a selected radio button `off` by clicking on
* it (see [[PDFRadioGroup.isOffToggleable]]).
*
* Note that some radio groups allow multiple radio buttons to be in the `on`
* state at the same type **if** they represent the same underlying value (see
* [[PDFRadioGroup.isMutuallyExclusive]]).
*/
export default class PDFRadioGroup extends PDFField {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFForm.getOptionList]] method, which will create an
* > instance of [[PDFOptionList]] for you.
*
* Create an instance of [[PDFOptionList]] from an existing acroRadioButton
* and ref
*
* @param acroRadioButton The underlying `PDFAcroRadioButton` for this
* radio group.
* @param ref The unique reference for this radio group.
* @param doc The document to which this radio group will belong.
*/
static of = (
acroRadioButton: PDFAcroRadioButton,
ref: PDFRef,
doc: PDFDocument,
) => new PDFRadioGroup(acroRadioButton, ref, doc);
/** The low-level PDFAcroRadioButton wrapped by this radio group. */
readonly acroField: PDFAcroRadioButton;
private constructor(
acroRadioButton: PDFAcroRadioButton,
ref: PDFRef,
doc: PDFDocument,
) {
super(acroRadioButton, ref, doc);
assertIs(acroRadioButton, 'acroRadioButton', [
[PDFAcroRadioButton, 'PDFAcroRadioButton'],
]);
this.acroField = acroRadioButton;
}
/**
* Get the list of available options for this radio group. Each option is
* represented by a radio button. These radio buttons are displayed at
* various locations in the document, potentially on different pages (though
* typically they are stacked horizontally or vertically on the same page).
* For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* const options = radioGroup.getOptions()
* console.log('Radio Group options:', options)
* ```
* @returns The options for this radio group.
*/
getOptions(): string[] {
const exportValues = this.acroField.getExportValues();
if (exportValues) {
const exportOptions = new Array<string>(exportValues.length);
for (let idx = 0, len = exportValues.length; idx < len; idx++) {
exportOptions[idx] = exportValues[idx].decodeText();
}
return exportOptions;
}
const onValues = this.acroField.getOnValues();
const onOptions = new Array<string>(onValues.length);
for (let idx = 0, len = onOptions.length; idx < len; idx++) {
onOptions[idx] = onValues[idx].decodeText();
}
return onOptions;
}
/**
* Get the selected option for this radio group. The selected option is
* represented by the radio button in this group that is turned on. At most
* one radio button in a group can be selected. If no buttons in this group
* are selected, `undefined` is returned.
* For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* const selected = radioGroup.getSelected()
* console.log('Selected radio button:', selected)
* ```
* @returns The selected option for this radio group.
*/
getSelected(): string | undefined {
const value = this.acroField.getValue();
if (value === PDFName.of('Off')) return undefined;
const exportValues = this.acroField.getExportValues();
if (exportValues) {
const onValues = this.acroField.getOnValues();
for (let idx = 0, len = onValues.length; idx < len; idx++) {
if (onValues[idx] === value) return exportValues[idx].decodeText();
}
}
return value.decodeText();
}
// // TODO: Figure out why this seems to crash Acrobat. Maybe it's because we
// // aren't removing the widget reference from the page's Annots?
// removeOption(option: string) {
// assertIs(option, 'option', ['string']);
// // TODO: Assert is valid `option`!
// const onValues = this.acroField.getOnValues();
// const exportValues = this.acroField.getExportValues();
// if (exportValues) {
// for (let idx = 0, len = exportValues.length; idx < len; idx++) {
// if (exportValues[idx].decodeText() === option) {
// this.acroField.removeWidget(idx);
// this.acroField.removeExportValue(idx);
// }
// }
// } else {
// for (let idx = 0, len = onValues.length; idx < len; idx++) {
// const value = onValues[idx];
// if (value.decodeText() === option) {
// this.acroField.removeWidget(idx);
// this.acroField.removeExportValue(idx);
// }
// }
// }
// }
/**
* Select an option for this radio group. This operation is analogous to a
* human user clicking one of the radio buttons in this group via a PDF
* reader to toggle it on. This method will update the underlying state of
* the radio group to indicate which option has been selected. PDF libraries
* and readers will be able to extract this value from the saved document and
* determine which option was selected.
*
* For example:
* ```js
* const radioGroup = form.getRadioGroup('best.superhero.radioGroup')
* radioGroup.select('One Punch Man')
* ```
*
* This method will mark this radio group as dirty, causing its appearance
* streams to be updated when either [[PDFDocument.save]] or
* [[PDFForm.updateFieldAppearances]] is called. The updated appearance
* streams will display a dot inside the widget of this check box field
* that represents the selected option.
*
* @param option The option to be selected.
*/
select(option: string) {
assertIs(option, 'option', ['string']);
const validOptions = this.getOptions();
assertIsOneOf(option, 'option', validOptions);
this.markAsDirty();
const onValues = this.acroField.getOnValues();
const exportValues = this.acroField.getExportValues();
if (exportValues) {
for (let idx = 0, len = exportValues.length; idx < len; idx++) {
if (exportValues[idx].decodeText() === option) {
this.acroField.setValue(onValues[idx]);
}
}
} else {
for (let idx = 0, len = onValues.length; idx < len; idx++) {
const value = onValues[idx];
if (value.decodeText() === option) this.acroField.setValue(value);
}
}
}
/**
* Clear any selected option for this dropdown. This will result in all
* radio buttons in this group being toggled off. This method will update
* the underlying state of the dropdown to indicate that no radio buttons
* have been selected.
* For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* radioGroup.clear()
* ```
* This method will mark this radio group as dirty. See
* [[PDFRadioGroup.select]] for more details about what this means.
*/
clear() {
this.markAsDirty();
this.acroField.setValue(PDFName.of('Off'));
}
/**
* Returns `true` if users can click on radio buttons in this group to toggle
* them off. The alternative is that once a user clicks on a radio button
* to select it, the only way to deselect it is by selecting on another radio
* button in the group. See [[PDFRadioGroup.enableOffToggling]] and
* [[PDFRadioGroup.disableOffToggling]]. For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* if (radioGroup.isOffToggleable()) console.log('Off toggling is enabled')
* ```
*/
isOffToggleable() {
return !this.acroField.hasFlag(AcroButtonFlags.NoToggleToOff);
}
/**
* Allow users to click on selected radio buttons in this group to toggle
* them off. For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* radioGroup.enableOffToggling()
* ```
* > **NOTE:** This feature is documented in the PDF specification
* > (Table 226). However, most PDF readers do not respect this option and
* > prevent users from toggling radio buttons off even when it is enabled.
* > At the time of this writing (9/6/2020) Mac's Preview software did
* > respect the option. Adobe Acrobat, Foxit Reader, and Google Chrome did
* > not.
*/
enableOffToggling() {
this.acroField.setFlagTo(AcroButtonFlags.NoToggleToOff, false);
}
/**
* Prevent users from clicking on selected radio buttons in this group to
* toggle them off. Clicking on a selected radio button will have no effect.
* The only way to deselect a selected radio button is to click on a
* different radio button in the group. For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* radioGroup.disableOffToggling()
* ```
*/
disableOffToggling() {
this.acroField.setFlagTo(AcroButtonFlags.NoToggleToOff, true);
}
/**
* Returns `true` if the radio buttons in this group are mutually exclusive.
* This means that when the user selects a radio button, only that specific
* button will be turned on. Even if other radio buttons in the group
* represent the same value, they will not be enabled. The alternative to
* this is that clicking a radio button will select that button along with
* any other radio buttons in the group that share the same value. See
* [[PDFRadioGroup.enableMutualExclusion]] and
* [[PDFRadioGroup.disableMutualExclusion]].
* For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* if (radioGroup.isMutuallyExclusive()) console.log('Mutual exclusion is enabled')
* ```
*/
isMutuallyExclusive() {
return !this.acroField.hasFlag(AcroButtonFlags.RadiosInUnison);
}
/**
* When the user clicks a radio button in this group it will be selected. In
* addition, any other radio buttons in this group that share the same
* underlying value will also be selected. For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* radioGroup.enableMutualExclusion()
* ```
* Note that this option must be enabled prior to adding options to the
* radio group. It does not currently apply retroactively to existing
* radio buttons in the group.
*/
enableMutualExclusion() {
this.acroField.setFlagTo(AcroButtonFlags.RadiosInUnison, false);
}
/**
* When the user clicks a radio button in this group only it will be selected.
* No other radio buttons in the group will be selected, even if they share
* the same underlying value. For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* radioGroup.disableMutualExclusion()
* ```
* Note that this option must be disabled prior to adding options to the
* radio group. It does not currently apply retroactively to existing
* radio buttons in the group.
*/
disableMutualExclusion() {
this.acroField.setFlagTo(AcroButtonFlags.RadiosInUnison, true);
}
/**
* Add a new radio button to this group on the specified page. For example:
* ```js
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const radioGroup = form.createRadioGroup('best.gundam')
*
* const options = {
* x: 50,
* width: 25,
* height: 25,
* textColor: rgb(1, 0, 0),
* backgroundColor: rgb(0, 1, 0),
* borderColor: rgb(0, 0, 1),
* borderWidth: 2,
* rotate: degrees(90),
* }
*
* radioGroup.addOptionToPage('Exia', page, { ...options, y: 50 })
* radioGroup.addOptionToPage('Dynames', page, { ...options, y: 110 })
* ```
* This will create a new radio button widget for this radio group field.
* @param option The option that the radio button widget represents.
* @param page The page to which the radio button widget should be added.
* @param options The options to be used when adding the radio button widget.
*/
addOptionToPage(
option: string,
page: PDFPage,
options?: FieldAppearanceOptions,
) {
assertIs(option, 'option', ['string']);
assertIs(page, 'page', [[PDFPage, 'PDFPage']]);
assertFieldAppearanceOptions(options);
// Create a widget for this radio button
const widget = this.createWidget({
x: options?.x ?? 0,
y: options?.y ?? 0,
width: options?.width ?? 50,
height: options?.height ?? 50,
textColor: options?.textColor ?? rgb(0, 0, 0),
backgroundColor: options?.backgroundColor ?? rgb(1, 1, 1),
borderColor: options?.borderColor ?? rgb(0, 0, 0),
borderWidth: options?.borderWidth ?? 1,
rotate: options?.rotate ?? degrees(0),
hidden: options?.hidden,
page: page.ref,
});
const widgetRef = this.doc.context.register(widget.dict);
// Add widget to this field
const apStateValue = this.acroField.addWidgetWithOpt(
widgetRef,
PDFHexString.fromText(option),
!this.isMutuallyExclusive(),
);
// Set appearance streams for widget
widget.setAppearanceState(PDFName.of('Off'));
this.updateWidgetAppearance(widget, apStateValue);
// Add widget to the given page
page.node.addAnnot(widgetRef);
}
/**
* Returns `true` if any of this group's radio button widgets do not have an
* appearance stream for their current state. For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* if (radioGroup.needsAppearancesUpdate()) console.log('Needs update')
* ```
* @returns Whether or not this radio group needs an appearance update.
*/
needsAppearancesUpdate(): boolean {
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const state = widget.getAppearanceState();
const normal = widget.getAppearances()?.normal;
if (!(normal instanceof PDFDict)) return true;
if (state && !normal.has(state)) return true;
}
return false;
}
/**
* Update the appearance streams for each of this group's radio button widgets
* using the default appearance provider for radio groups. For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* radioGroup.defaultUpdateAppearances()
* ```
*/
defaultUpdateAppearances() {
this.updateAppearances();
}
// rg.updateAppearances((field: any, widget: any) => {
// assert(field === rg);
// assert(widget instanceof PDFWidgetAnnotation);
// return { on: [...rectangle, ...circle], off: [...rectangle, ...circle] };
// });
/**
* Update the appearance streams for each of this group's radio button widgets
* using the given appearance provider. If no `provider` is passed, the
* default appearance provider for radio groups will be used. For example:
* ```js
* const radioGroup = form.getRadioGroup('some.radioGroup.field')
* radioGroup.updateAppearances((field, widget) => {
* ...
* return {
* normal: { on: drawRadioButton(...), off: drawRadioButton(...) },
* down: { on: drawRadioButton(...), off: drawRadioButton(...) },
* }
* })
* ```
* @param provider Optionally, the appearance provider to be used for
* generating the contents of the appearance streams.
*/
updateAppearances(provider?: AppearanceProviderFor<PDFRadioGroup>) {
assertOrUndefined(provider, 'provider', [Function]);
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const onValue = widget.getOnValue();
if (!onValue) continue;
this.updateWidgetAppearance(widget, onValue, provider);
}
}
private updateWidgetAppearance(
widget: PDFWidgetAnnotation,
onValue: PDFName,
provider?: AppearanceProviderFor<PDFRadioGroup>,
) {
const apProvider = provider ?? defaultRadioGroupAppearanceProvider;
const appearances = normalizeAppearance(apProvider(this, widget));
this.updateOnOffWidgetAppearance(widget, onValue, appearances);
}
}

53
node_modules/pdf-lib/src/api/form/PDFSignature.ts generated vendored Normal file
View File

@@ -0,0 +1,53 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFField from 'src/api/form/PDFField';
import { PDFRef, PDFAcroSignature } from 'src/core';
import { assertIs } from 'src/utils';
/**
* Represents a signature field of a [[PDFForm]].
*
* [[PDFSignature]] fields are digital signatures. `pdf-lib` does not
* currently provide any specialized APIs for creating digital signatures or
* reading the contents of existing digital signatures.
*/
export default class PDFSignature extends PDFField {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFForm.getSignature]] method, which will create an
* > instance of [[PDFSignature]] for you.
*
* Create an instance of [[PDFSignature]] from an existing acroSignature and
* ref
*
* @param acroSignature The underlying `PDFAcroSignature` for this signature.
* @param ref The unique reference for this signature.
* @param doc The document to which this signature will belong.
*/
static of = (
acroSignature: PDFAcroSignature,
ref: PDFRef,
doc: PDFDocument,
) => new PDFSignature(acroSignature, ref, doc);
/** The low-level PDFAcroSignature wrapped by this signature. */
readonly acroField: PDFAcroSignature;
private constructor(
acroSignature: PDFAcroSignature,
ref: PDFRef,
doc: PDFDocument,
) {
super(acroSignature, ref, doc);
assertIs(acroSignature, 'acroSignature', [
[PDFAcroSignature, 'PDFAcroSignature'],
]);
this.acroField = acroSignature;
}
needsAppearancesUpdate() {
return false;
}
}

826
node_modules/pdf-lib/src/api/form/PDFTextField.ts generated vendored Normal file
View File

@@ -0,0 +1,826 @@
import PDFDocument from 'src/api/PDFDocument';
import PDFPage from 'src/api/PDFPage';
import PDFFont from 'src/api/PDFFont';
import PDFImage from 'src/api/PDFImage';
import PDFField, {
FieldAppearanceOptions,
assertFieldAppearanceOptions,
} from 'src/api/form/PDFField';
import {
AppearanceProviderFor,
normalizeAppearance,
defaultTextFieldAppearanceProvider,
} from 'src/api/form/appearances';
import { rgb } from 'src/api/colors';
import { degrees } from 'src/api/rotations';
import {
RichTextFieldReadError,
ExceededMaxLengthError,
InvalidMaxLengthError,
} from 'src/api/errors';
import { ImageAlignment } from 'src/api/image/alignment';
import { TextAlignment } from 'src/api/text/alignment';
import {
PDFHexString,
PDFRef,
PDFStream,
PDFAcroText,
AcroTextFlags,
PDFWidgetAnnotation,
} from 'src/core';
import {
assertIs,
assertIsOneOf,
assertOrUndefined,
assertPositive,
assertRangeOrUndefined,
} from 'src/utils';
/**
* Represents a text field of a [[PDFForm]].
*
* [[PDFTextField]] fields are boxes that display text entered by the user. The
* purpose of a text field is to enable users to enter text or view text values
* in the document prefilled by software. Users can click on a text field and
* input text via their keyboard. Some text fields allow multiple lines of text
* to be entered (see [[PDFTextField.isMultiline]]).
*/
export default class PDFTextField extends PDFField {
/**
* > **NOTE:** You probably don't want to call this method directly. Instead,
* > consider using the [[PDFForm.getTextField]] method, which will create an
* > instance of [[PDFTextField]] for you.
*
* Create an instance of [[PDFTextField]] from an existing acroText and ref
*
* @param acroText The underlying `PDFAcroText` for this text field.
* @param ref The unique reference for this text field.
* @param doc The document to which this text field will belong.
*/
static of = (acroText: PDFAcroText, ref: PDFRef, doc: PDFDocument) =>
new PDFTextField(acroText, ref, doc);
/** The low-level PDFAcroText wrapped by this text field. */
readonly acroField: PDFAcroText;
private constructor(acroText: PDFAcroText, ref: PDFRef, doc: PDFDocument) {
super(acroText, ref, doc);
assertIs(acroText, 'acroText', [[PDFAcroText, 'PDFAcroText']]);
this.acroField = acroText;
}
/**
* Get the text that this field contains. This text is visible to users who
* view this field in a PDF reader.
*
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* const text = textField.getText()
* console.log('Text field contents:', text)
* ```
*
* Note that if this text field contains no underlying value, `undefined`
* will be returned. Text fields may also contain an underlying value that
* is simply an empty string (`''`). This detail is largely irrelevant for
* most applications. In general, you'll want to treat both cases the same
* way and simply consider the text field to be empty. In either case, the
* text field will appear empty to users when viewed in a PDF reader.
*
* An error will be thrown if this is a rich text field. `pdf-lib` does not
* support reading rich text fields. Nor do most PDF readers and writers.
* Rich text fields are based on XFA (XML Forms Architecture). Relatively few
* PDFs use rich text fields or XFA. Unlike PDF itself, XFA is not an ISO
* standard. XFA has been deprecated in PDF 2.0:
* * https://en.wikipedia.org/wiki/XFA
* * http://blog.pdfshareforms.com/pdf-2-0-release-bid-farewell-xfa-forms/
*
* @returns The text contained in this text field.
*/
getText(): string | undefined {
const value = this.acroField.getValue();
if (!value && this.isRichFormatted()) {
throw new RichTextFieldReadError(this.getName());
}
return value?.decodeText();
}
/**
* Set the text for this field. This operation is analogous to a human user
* clicking on the text field in a PDF reader and typing in text via their
* keyboard. This method will update the underlying state of the text field
* to indicate what text has been set. PDF libraries and readers will be able
* to extract these values from the saved document and determine what text
* was set.
*
* For example:
* ```js
* const textField = form.getTextField('best.superhero.text.field')
* textField.setText('One Punch Man')
* ```
*
* This method will mark this text field as dirty, causing its appearance
* streams to be updated when either [[PDFDocument.save]] or
* [[PDFForm.updateFieldAppearances]] is called. The updated streams will
* display the text this field contains inside the widgets of this text
* field.
*
* **IMPORTANT:** The default font used to update appearance streams is
* [[StandardFonts.Helvetica]]. Note that this is a WinAnsi font. This means
* that encoding errors will be thrown if this field contains text outside
* the WinAnsi character set (the latin alphabet).
*
* Embedding a custom font and passing it to
* [[PDFForm.updateFieldAppearances]] or [[PDFTextField.updateAppearances]]
* allows you to generate appearance streams with characters outside the
* latin alphabet (assuming the custom font supports them).
*
* If this is a rich text field, it will be converted to a standard text
* field in order to set the text. `pdf-lib` does not support writing rich
* text strings. Nor do most PDF readers and writers. See
* [[PDFTextField.getText]] for more information about rich text fields and
* their deprecation in PDF 2.0.
*
* @param text The text this field should contain.
*/
setText(text: string | undefined) {
assertOrUndefined(text, 'text', ['string']);
const maxLength = this.getMaxLength();
if (maxLength !== undefined && text && text.length > maxLength) {
throw new ExceededMaxLengthError(text.length, maxLength, this.getName());
}
this.markAsDirty();
this.disableRichFormatting();
if (text) {
this.acroField.setValue(PDFHexString.fromText(text));
} else {
this.acroField.removeValue();
}
}
/**
* Get the alignment for this text field. This value represents the
* justification of the text when it is displayed to the user in PDF readers.
* There are three possible alignments: left, center, and right. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* const alignment = textField.getAlignment()
* if (alignment === TextAlignment.Left) console.log('Text is left justified')
* if (alignment === TextAlignment.Center) console.log('Text is centered')
* if (alignment === TextAlignment.Right) console.log('Text is right justified')
* ```
* @returns The alignment of this text field.
*/
getAlignment(): TextAlignment {
const quadding = this.acroField.getQuadding();
// prettier-ignore
return (
quadding === 0 ? TextAlignment.Left
: quadding === 1 ? TextAlignment.Center
: quadding === 2 ? TextAlignment.Right
: TextAlignment.Left
);
}
/**
* Set the alignment for this text field. This will determine the
* justification of the text when it is displayed to the user in PDF readers.
* There are three possible alignments: left, center, and right. For example:
* ```js
* const textField = form.getTextField('some.text.field')
*
* // Text will be left justified when displayed
* textField.setAlignment(TextAlignment.Left)
*
* // Text will be centered when displayed
* textField.setAlignment(TextAlignment.Center)
*
* // Text will be right justified when displayed
* textField.setAlignment(TextAlignment.Right)
* ```
* This method will mark this text field as dirty. See
* [[PDFTextField.setText]] for more details about what this means.
* @param alignment The alignment for this text field.
*/
setAlignment(alignment: TextAlignment) {
assertIsOneOf(alignment, 'alignment', TextAlignment);
this.markAsDirty();
this.acroField.setQuadding(alignment);
}
/**
* Get the maximum length of this field. This value represents the maximum
* number of characters that can be typed into this field by the user. If
* this field does not have a maximum length, `undefined` is returned.
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* const maxLength = textField.getMaxLength()
* if (maxLength === undefined) console.log('No max length')
* else console.log(`Max length is ${maxLength}`)
* ```
* @returns The maximum number of characters allowed in this field, or
* `undefined` if no limit exists.
*/
getMaxLength(): number | undefined {
return this.acroField.getMaxLength();
}
/**
* Set the maximum length of this field. This limits the number of characters
* that can be typed into this field by the user. This also limits the length
* of the string that can be passed to [[PDFTextField.setText]]. This limit
* can be removed by passing `undefined` as `maxLength`. For example:
* ```js
* const textField = form.getTextField('some.text.field')
*
* // Allow between 0 and 5 characters to be entered
* textField.setMaxLength(5)
*
* // Allow any number of characters to be entered
* textField.setMaxLength(undefined)
* ```
* This method will mark this text field as dirty. See
* [[PDFTextField.setText]] for more details about what this means.
* @param maxLength The maximum number of characters allowed in this field, or
* `undefined` to remove the limit.
*/
setMaxLength(maxLength?: number) {
assertRangeOrUndefined(maxLength, 'maxLength', 0, Number.MAX_SAFE_INTEGER);
this.markAsDirty();
if (maxLength === undefined) {
this.acroField.removeMaxLength();
} else {
const text = this.getText();
if (text && text.length > maxLength) {
throw new InvalidMaxLengthError(text.length, maxLength, this.getName());
}
this.acroField.setMaxLength(maxLength);
}
}
/**
* Remove the maximum length for this text field. This allows any number of
* characters to be typed into this field by the user. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.removeMaxLength()
* ```
* Calling this method is equivalent to passing `undefined` to
* [[PDFTextField.setMaxLength]].
*/
removeMaxLength() {
this.markAsDirty();
this.acroField.removeMaxLength();
}
/**
* Display an image inside the bounds of this text field's widgets. For example:
* ```js
* const pngImage = await pdfDoc.embedPng(...)
* const textField = form.getTextField('some.text.field')
* textField.setImage(pngImage)
* ```
* This will update the appearances streams for each of this text field's widgets.
* @param image The image that should be displayed.
*/
setImage(image: PDFImage) {
const fieldAlignment = this.getAlignment();
// prettier-ignore
const alignment =
fieldAlignment === TextAlignment.Center ? ImageAlignment.Center
: fieldAlignment === TextAlignment.Right ? ImageAlignment.Right
: ImageAlignment.Left;
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const streamRef = this.createImageAppearanceStream(
widget,
image,
alignment,
);
this.updateWidgetAppearances(widget, { normal: streamRef });
}
this.markAsClean();
}
/**
* Set the font size for this field. Larger font sizes will result in larger
* text being displayed when PDF readers render this text field. Font sizes
* may be integer or floating point numbers. Supplying a negative font size
* will cause this method to throw an error.
*
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.setFontSize(4)
* textField.setFontSize(15.7)
* ```
*
* > This method depends upon the existence of a default appearance
* > (`/DA`) string. If this field does not have a default appearance string,
* > or that string does not contain a font size (via the `Tf` operator),
* > then this method will throw an error.
*
* @param fontSize The font size to be used when rendering text in this field.
*/
setFontSize(fontSize: number) {
assertPositive(fontSize, 'fontSize');
this.acroField.setFontSize(fontSize);
this.markAsDirty();
}
/**
* Returns `true` if each line of text is shown on a new line when this
* field is displayed in a PDF reader. The alternative is that all lines of
* text are merged onto a single line when displayed. See
* [[PDFTextField.enableMultiline]] and [[PDFTextField.disableMultiline]].
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* if (textField.isMultiline()) console.log('Multiline is enabled')
* ```
* @returns Whether or not this is a multiline text field.
*/
isMultiline(): boolean {
return this.acroField.hasFlag(AcroTextFlags.Multiline);
}
/**
* Display each line of text on a new line when this field is displayed in a
* PDF reader. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.enableMultiline()
* ```
* This method will mark this text field as dirty. See
* [[PDFTextField.setText]] for more details about what this means.
*/
enableMultiline() {
this.markAsDirty();
this.acroField.setFlagTo(AcroTextFlags.Multiline, true);
}
/**
* Display each line of text on the same line when this field is displayed
* in a PDF reader. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.disableMultiline()
* ```
* This method will mark this text field as dirty. See
* [[PDFTextField.setText]] for more details about what this means.
*/
disableMultiline() {
this.markAsDirty();
this.acroField.setFlagTo(AcroTextFlags.Multiline, false);
}
/**
* Returns `true` if this is a password text field. This means that the field
* is intended for storing a secure password. See
* [[PDFTextField.enablePassword]] and [[PDFTextField.disablePassword]].
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* if (textField.isPassword()) console.log('Password is enabled')
* ```
* @returns Whether or not this is a password text field.
*/
isPassword(): boolean {
return this.acroField.hasFlag(AcroTextFlags.Password);
}
/**
* Indicate that this text field is intended for storing a secure password.
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.enablePassword()
* ```
* Values entered into password text fields should not be displayed on the
* screen by PDF readers. Most PDF readers will display the value as
* asterisks or bullets. PDF readers should never store values entered by the
* user into password text fields. Similarly, applications should not
* write data to a password text field.
*
* **Please note that this method does not cause entered values to be
* encrypted or secured in any way! It simply sets a flag that PDF software
* and readers can access to determine the _purpose_ of this field.**
*/
enablePassword() {
this.acroField.setFlagTo(AcroTextFlags.Password, true);
}
/**
* Indicate that this text field is **not** intended for storing a secure
* password. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.disablePassword()
* ```
*/
disablePassword() {
this.acroField.setFlagTo(AcroTextFlags.Password, false);
}
/**
* Returns `true` if the contents of this text field represent a file path.
* See [[PDFTextField.enableFileSelection]] and
* [[PDFTextField.disableFileSelection]]. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* if (textField.isFileSelector()) console.log('Is a file selector')
* ```
* @returns Whether or not this field should contain file paths.
*/
isFileSelector(): boolean {
return this.acroField.hasFlag(AcroTextFlags.FileSelect);
}
/**
* Indicate that this text field is intended to store a file path. The
* contents of the file stored at that path should be submitted as the value
* of the field. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.enableFileSelection()
* ```
*/
enableFileSelection() {
this.acroField.setFlagTo(AcroTextFlags.FileSelect, true);
}
/**
* Indicate that this text field is **not** intended to store a file path.
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.disableFileSelection()
* ```
*/
disableFileSelection() {
this.acroField.setFlagTo(AcroTextFlags.FileSelect, false);
}
/**
* Returns `true` if the text entered in this field should be spell checked
* by PDF readers. See [[PDFTextField.enableSpellChecking]] and
* [[PDFTextField.disableSpellChecking]]. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* if (textField.isSpellChecked()) console.log('Spell checking is enabled')
* ```
* @returns Whether or not this field should be spell checked.
*/
isSpellChecked(): boolean {
return !this.acroField.hasFlag(AcroTextFlags.DoNotSpellCheck);
}
/**
* Allow PDF readers to spell check the text entered in this field.
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.enableSpellChecking()
* ```
*/
enableSpellChecking() {
this.acroField.setFlagTo(AcroTextFlags.DoNotSpellCheck, false);
}
/**
* Do not allow PDF readers to spell check the text entered in this field.
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.disableSpellChecking()
* ```
*/
disableSpellChecking() {
this.acroField.setFlagTo(AcroTextFlags.DoNotSpellCheck, true);
}
/**
* Returns `true` if PDF readers should allow the user to scroll the text
* field when its contents do not fit within the field's view bounds. See
* [[PDFTextField.enableScrolling]] and [[PDFTextField.disableScrolling]].
* For example:
* ```js
* const textField = form.getTextField('some.text.field')
* if (textField.isScrollable()) console.log('Scrolling is enabled')
* ```
* @returns Whether or not the field is scrollable in PDF readers.
*/
isScrollable(): boolean {
return !this.acroField.hasFlag(AcroTextFlags.DoNotScroll);
}
/**
* Allow PDF readers to present a scroll bar to the user when the contents
* of this text field do not fit within its view bounds. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.enableScrolling()
* ```
* A horizontal scroll bar should be shown for singleline fields. A vertical
* scroll bar should be shown for multiline fields.
*/
enableScrolling() {
this.acroField.setFlagTo(AcroTextFlags.DoNotScroll, false);
}
/**
* Do not allow PDF readers to present a scroll bar to the user when the
* contents of this text field do not fit within its view bounds. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.disableScrolling()
* ```
*/
disableScrolling() {
this.acroField.setFlagTo(AcroTextFlags.DoNotScroll, true);
}
/**
* Returns `true` if this is a combed text field. This means that the field
* is split into `n` equal size cells with one character in each (where `n`
* is equal to the max length of the text field). The result is that all
* characters in this field are displayed an equal distance apart from one
* another. See [[PDFTextField.enableCombing]] and
* [[PDFTextField.disableCombing]]. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* if (textField.isCombed()) console.log('Combing is enabled')
* ```
* Note that in order for a text field to be combed, the following must be
* true (in addition to enabling combing):
* * It must not be a multiline field (see [[PDFTextField.isMultiline]])
* * It must not be a password field (see [[PDFTextField.isPassword]])
* * It must not be a file selector field (see [[PDFTextField.isFileSelector]])
* * It must have a max length defined (see [[PDFTextField.setMaxLength]])
* @returns Whether or not this field is combed.
*/
isCombed(): boolean {
return (
this.acroField.hasFlag(AcroTextFlags.Comb) &&
!this.isMultiline() &&
!this.isPassword() &&
!this.isFileSelector() &&
this.getMaxLength() !== undefined
);
}
/**
* Split this field into `n` equal size cells with one character in each
* (where `n` is equal to the max length of the text field). This will cause
* all characters in the field to be displayed an equal distance apart from
* one another. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.enableCombing()
* ```
*
* In addition to calling this method, text fields must have a max length
* defined in order to be combed (see [[PDFTextField.setMaxLength]]).
*
* This method will also call the following three methods internally:
* * [[PDFTextField.disableMultiline]]
* * [[PDFTextField.disablePassword]]
* * [[PDFTextField.disableFileSelection]]
*
* This method will mark this text field as dirty. See
* [[PDFTextField.setText]] for more details about what this means.
*/
enableCombing() {
if (this.getMaxLength() === undefined) {
const msg = `PDFTextFields must have a max length in order to be combed`;
console.warn(msg);
}
this.markAsDirty();
this.disableMultiline();
this.disablePassword();
this.disableFileSelection();
this.acroField.setFlagTo(AcroTextFlags.Comb, true);
}
/**
* Turn off combing for this text field. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.disableCombing()
* ```
* See [[PDFTextField.isCombed]] and [[PDFTextField.enableCombing]] for more
* information about what combing is.
*
* This method will mark this text field as dirty. See
* [[PDFTextField.setText]] for more details about what this means.
*/
disableCombing() {
this.markAsDirty();
this.acroField.setFlagTo(AcroTextFlags.Comb, false);
}
/**
* Returns `true` if this text field contains rich text. See
* [[PDFTextField.enableRichFormatting]] and
* [[PDFTextField.disableRichFormatting]]. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* if (textField.isRichFormatted()) console.log('Rich formatting enabled')
* ```
* @returns Whether or not this field contains rich text.
*/
isRichFormatted(): boolean {
return this.acroField.hasFlag(AcroTextFlags.RichText);
}
/**
* Indicate that this field contains XFA data - or rich text. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.enableRichFormatting()
* ```
* Note that `pdf-lib` does not support reading or writing rich text fields.
* Nor do most PDF readers and writers. Rich text fields are based on XFA
* (XML Forms Architecture). Relatively few PDFs use rich text fields or XFA.
* Unlike PDF itself, XFA is not an ISO standard. XFA has been deprecated in
* PDF 2.0:
* * https://en.wikipedia.org/wiki/XFA
* * http://blog.pdfshareforms.com/pdf-2-0-release-bid-farewell-xfa-forms/
*/
enableRichFormatting() {
this.acroField.setFlagTo(AcroTextFlags.RichText, true);
}
/**
* Indicate that this is a standard text field that does not XFA data (rich
* text). For example:
* ```js
* const textField = form.getTextField('some.text.field')
* textField.disableRichFormatting()
* ```
*/
disableRichFormatting() {
this.acroField.setFlagTo(AcroTextFlags.RichText, false);
}
/**
* Show this text field on the specified page. For example:
* ```js
* const ubuntuFont = await pdfDoc.embedFont(ubuntuFontBytes)
* const page = pdfDoc.addPage()
*
* const form = pdfDoc.getForm()
* const textField = form.createTextField('best.gundam')
* textField.setText('Exia')
*
* textField.addToPage(page, {
* x: 50,
* y: 75,
* width: 200,
* height: 100,
* textColor: rgb(1, 0, 0),
* backgroundColor: rgb(0, 1, 0),
* borderColor: rgb(0, 0, 1),
* borderWidth: 2,
* rotate: degrees(90),
* font: ubuntuFont,
* })
* ```
* This will create a new widget for this text field.
* @param page The page to which this text field widget should be added.
* @param options The options to be used when adding this text field widget.
*/
addToPage(page: PDFPage, options?: FieldAppearanceOptions) {
assertIs(page, 'page', [[PDFPage, 'PDFPage']]);
assertFieldAppearanceOptions(options);
if (!options) options = {};
if (!('textColor' in options)) options.textColor = rgb(0, 0, 0);
if (!('backgroundColor' in options)) options.backgroundColor = rgb(1, 1, 1);
if (!('borderColor' in options)) options.borderColor = rgb(0, 0, 0);
if (!('borderWidth' in options)) options.borderWidth = 1;
// Create a widget for this text field
const widget = this.createWidget({
x: options.x ?? 0,
y: options.y ?? 0,
width: options.width ?? 200,
height: options.height ?? 50,
textColor: options.textColor,
backgroundColor: options.backgroundColor,
borderColor: options.borderColor,
borderWidth: options.borderWidth ?? 0,
rotate: options.rotate ?? degrees(0),
hidden: options.hidden,
page: page.ref,
});
const widgetRef = this.doc.context.register(widget.dict);
// Add widget to this field
this.acroField.addWidget(widgetRef);
// Set appearance streams for widget
const font = options.font ?? this.doc.getForm().getDefaultFont();
this.updateWidgetAppearance(widget, font);
// Add widget to the given page
page.node.addAnnot(widgetRef);
}
/**
* Returns `true` if this text field has been marked as dirty, or if any of
* this text field's widgets do not have an appearance stream. For example:
* ```js
* const textField = form.getTextField('some.text.field')
* if (textField.needsAppearancesUpdate()) console.log('Needs update')
* ```
* @returns Whether or not this text field needs an appearance update.
*/
needsAppearancesUpdate(): boolean {
if (this.isDirty()) return true;
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const hasAppearances =
widget.getAppearances()?.normal instanceof PDFStream;
if (!hasAppearances) return true;
}
return false;
}
/**
* Update the appearance streams for each of this text field's widgets using
* the default appearance provider for text fields. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const textField = form.getTextField('some.text.field')
* textField.defaultUpdateAppearances(helvetica)
* ```
* @param font The font to be used for creating the appearance streams.
*/
defaultUpdateAppearances(font: PDFFont) {
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
this.updateAppearances(font);
}
/**
* Update the appearance streams for each of this text field's widgets using
* the given appearance provider. If no `provider` is passed, the default
* appearance provider for text fields will be used. For example:
* ```js
* const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica)
* const textField = form.getTextField('some.text.field')
* textField.updateAppearances(helvetica, (field, widget, font) => {
* ...
* return drawTextField(...)
* })
* ```
* @param font The font to be used for creating the appearance streams.
* @param provider Optionally, the appearance provider to be used for
* generating the contents of the appearance streams.
*/
updateAppearances(
font: PDFFont,
provider?: AppearanceProviderFor<PDFTextField>,
) {
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
assertOrUndefined(provider, 'provider', [Function]);
const widgets = this.acroField.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
this.updateWidgetAppearance(widget, font, provider);
}
this.markAsClean();
}
private updateWidgetAppearance(
widget: PDFWidgetAnnotation,
font: PDFFont,
provider?: AppearanceProviderFor<PDFTextField>,
) {
const apProvider = provider ?? defaultTextFieldAppearanceProvider;
const appearances = normalizeAppearance(apProvider(this, widget, font));
this.updateWidgetAppearanceWithFont(widget, font, appearances);
}
}

665
node_modules/pdf-lib/src/api/form/appearances.ts generated vendored Normal file
View File

@@ -0,0 +1,665 @@
import { PDFOperator, PDFWidgetAnnotation } from 'src/core';
import PDFFont from 'src/api/PDFFont';
import PDFButton from 'src/api/form/PDFButton';
import PDFCheckBox from 'src/api/form/PDFCheckBox';
import PDFDropdown from 'src/api/form/PDFDropdown';
import PDFField from 'src/api/form/PDFField';
import PDFOptionList from 'src/api/form/PDFOptionList';
import PDFRadioGroup from 'src/api/form/PDFRadioGroup';
import PDFSignature from 'src/api/form/PDFSignature';
import PDFTextField from 'src/api/form/PDFTextField';
import {
drawCheckBox,
rotateInPlace,
drawRadioButton,
drawButton,
drawTextField,
drawOptionList,
} from 'src/api/operations';
import {
rgb,
componentsToColor,
setFillingColor,
grayscale,
cmyk,
Color,
} from 'src/api/colors';
import { reduceRotation, adjustDimsForRotation } from 'src/api/rotations';
import {
layoutMultilineText,
layoutCombedText,
TextPosition,
layoutSinglelineText,
} from 'src/api/text/layout';
import { TextAlignment } from 'src/api/text/alignment';
import { setFontAndSize } from 'src/api/operators';
import { findLastMatch } from 'src/utils';
/*********************** Appearance Provider Types ****************************/
type CheckBoxAppearanceProvider = (
checkBox: PDFCheckBox,
widget: PDFWidgetAnnotation,
) => AppearanceOrMapping<{
on: PDFOperator[];
off: PDFOperator[];
}>;
type RadioGroupAppearanceProvider = (
radioGroup: PDFRadioGroup,
widget: PDFWidgetAnnotation,
) => AppearanceOrMapping<{
on: PDFOperator[];
off: PDFOperator[];
}>;
type ButtonAppearanceProvider = (
button: PDFButton,
widget: PDFWidgetAnnotation,
font: PDFFont,
) => AppearanceOrMapping<PDFOperator[]>;
type DropdownAppearanceProvider = (
dropdown: PDFDropdown,
widget: PDFWidgetAnnotation,
font: PDFFont,
) => AppearanceOrMapping<PDFOperator[]>;
type OptionListAppearanceProvider = (
optionList: PDFOptionList,
widget: PDFWidgetAnnotation,
font: PDFFont,
) => AppearanceOrMapping<PDFOperator[]>;
type TextFieldAppearanceProvider = (
textField: PDFTextField,
widget: PDFWidgetAnnotation,
font: PDFFont,
) => AppearanceOrMapping<PDFOperator[]>;
type SignatureAppearanceProvider = (
signature: PDFSignature,
widget: PDFWidgetAnnotation,
font: PDFFont,
) => AppearanceOrMapping<PDFOperator[]>;
/******************* Appearance Provider Utility Types ************************/
export type AppearanceMapping<T> = { normal: T; rollover?: T; down?: T };
type AppearanceOrMapping<T> = T | AppearanceMapping<T>;
// prettier-ignore
export type AppearanceProviderFor<T extends PDFField> =
T extends PDFCheckBox ? CheckBoxAppearanceProvider
: T extends PDFRadioGroup ? RadioGroupAppearanceProvider
: T extends PDFButton ? ButtonAppearanceProvider
: T extends PDFDropdown ? DropdownAppearanceProvider
: T extends PDFOptionList ? OptionListAppearanceProvider
: T extends PDFTextField ? TextFieldAppearanceProvider
: T extends PDFSignature ? SignatureAppearanceProvider
: never;
/********************* Appearance Provider Functions **************************/
export const normalizeAppearance = <T>(
appearance: T | AppearanceMapping<T>,
): AppearanceMapping<T> => {
if ('normal' in appearance) return appearance;
return { normal: appearance };
};
// Examples:
// `/Helv 12 Tf` -> ['/Helv 12 Tf', 'Helv', '12']
// `/HeBo 8.00 Tf` -> ['/HeBo 8 Tf', 'HeBo', '8.00']
const tfRegex = /\/([^\0\t\n\f\r\ ]+)[\0\t\n\f\r\ ]+(\d*\.\d+|\d+)[\0\t\n\f\r\ ]+Tf/;
const getDefaultFontSize = (field: {
getDefaultAppearance(): string | undefined;
}) => {
const da = field.getDefaultAppearance() ?? '';
const daMatch = findLastMatch(da, tfRegex).match ?? [];
const defaultFontSize = Number(daMatch[2]);
return isFinite(defaultFontSize) ? defaultFontSize : undefined;
};
// Examples:
// `0.3 g` -> ['0.3', 'g']
// `0.3 1 .3 rg` -> ['0.3', '1', '.3', 'rg']
// `0.3 1 .3 0 k` -> ['0.3', '1', '.3', '0', 'k']
const colorRegex = /(\d*\.\d+|\d+)[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]+(g|rg|k)/;
const getDefaultColor = (field: {
getDefaultAppearance(): string | undefined;
}) => {
const da = field.getDefaultAppearance() ?? '';
const daMatch = findLastMatch(da, colorRegex).match;
const [, c1, c2, c3, c4, colorSpace] = daMatch ?? [];
if (colorSpace === 'g' && c1) {
return grayscale(Number(c1));
}
if (colorSpace === 'rg' && c1 && c2 && c3) {
return rgb(Number(c1), Number(c2), Number(c3));
}
if (colorSpace === 'k' && c1 && c2 && c3 && c4) {
return cmyk(Number(c1), Number(c2), Number(c3), Number(c4));
}
return undefined;
};
const updateDefaultAppearance = (
field: { setDefaultAppearance(appearance: string): void },
color: Color,
font?: PDFFont,
fontSize: number = 0,
) => {
const da = [
setFillingColor(color).toString(),
setFontAndSize(font?.name ?? 'dummy__noop', fontSize).toString(),
].join('\n');
field.setDefaultAppearance(da);
};
export const defaultCheckBoxAppearanceProvider: AppearanceProviderFor<PDFCheckBox> = (
checkBox,
widget,
) => {
// The `/DA` entry can be at the widget or field level - so we handle both
const widgetColor = getDefaultColor(widget);
const fieldColor = getDefaultColor(checkBox.acroField);
const rectangle = widget.getRectangle();
const ap = widget.getAppearanceCharacteristics();
const bs = widget.getBorderStyle();
const borderWidth = bs?.getWidth() ?? 0;
const rotation = reduceRotation(ap?.getRotation());
const { width, height } = adjustDimsForRotation(rectangle, rotation);
const rotate = rotateInPlace({ ...rectangle, rotation });
const black = rgb(0, 0, 0);
const borderColor = componentsToColor(ap?.getBorderColor()) ?? black;
const normalBackgroundColor = componentsToColor(ap?.getBackgroundColor());
const downBackgroundColor = componentsToColor(ap?.getBackgroundColor(), 0.8);
// Update color
const textColor = widgetColor ?? fieldColor ?? black;
if (widgetColor) {
updateDefaultAppearance(widget, textColor);
} else {
updateDefaultAppearance(checkBox.acroField, textColor);
}
const options = {
x: 0 + borderWidth / 2,
y: 0 + borderWidth / 2,
width: width - borderWidth,
height: height - borderWidth,
thickness: 1.5,
borderWidth,
borderColor,
markColor: textColor,
};
return {
normal: {
on: [
...rotate,
...drawCheckBox({
...options,
color: normalBackgroundColor,
filled: true,
}),
],
off: [
...rotate,
...drawCheckBox({
...options,
color: normalBackgroundColor,
filled: false,
}),
],
},
down: {
on: [
...rotate,
...drawCheckBox({
...options,
color: downBackgroundColor,
filled: true,
}),
],
off: [
...rotate,
...drawCheckBox({
...options,
color: downBackgroundColor,
filled: false,
}),
],
},
};
};
export const defaultRadioGroupAppearanceProvider: AppearanceProviderFor<PDFRadioGroup> = (
radioGroup,
widget,
) => {
// The `/DA` entry can be at the widget or field level - so we handle both
const widgetColor = getDefaultColor(widget);
const fieldColor = getDefaultColor(radioGroup.acroField);
const rectangle = widget.getRectangle();
const ap = widget.getAppearanceCharacteristics();
const bs = widget.getBorderStyle();
const borderWidth = bs?.getWidth() ?? 0;
const rotation = reduceRotation(ap?.getRotation());
const { width, height } = adjustDimsForRotation(rectangle, rotation);
const rotate = rotateInPlace({ ...rectangle, rotation });
const black = rgb(0, 0, 0);
const borderColor = componentsToColor(ap?.getBorderColor()) ?? black;
const normalBackgroundColor = componentsToColor(ap?.getBackgroundColor());
const downBackgroundColor = componentsToColor(ap?.getBackgroundColor(), 0.8);
// Update color
const textColor = widgetColor ?? fieldColor ?? black;
if (widgetColor) {
updateDefaultAppearance(widget, textColor);
} else {
updateDefaultAppearance(radioGroup.acroField, textColor);
}
const options = {
x: width / 2,
y: height / 2,
width: width - borderWidth,
height: height - borderWidth,
borderWidth,
borderColor,
dotColor: textColor,
};
return {
normal: {
on: [
...rotate,
...drawRadioButton({
...options,
color: normalBackgroundColor,
filled: true,
}),
],
off: [
...rotate,
...drawRadioButton({
...options,
color: normalBackgroundColor,
filled: false,
}),
],
},
down: {
on: [
...rotate,
...drawRadioButton({
...options,
color: downBackgroundColor,
filled: true,
}),
],
off: [
...rotate,
...drawRadioButton({
...options,
color: downBackgroundColor,
filled: false,
}),
],
},
};
};
export const defaultButtonAppearanceProvider: AppearanceProviderFor<PDFButton> = (
button,
widget,
font,
) => {
// The `/DA` entry can be at the widget or field level - so we handle both
const widgetColor = getDefaultColor(widget);
const fieldColor = getDefaultColor(button.acroField);
const widgetFontSize = getDefaultFontSize(widget);
const fieldFontSize = getDefaultFontSize(button.acroField);
const rectangle = widget.getRectangle();
const ap = widget.getAppearanceCharacteristics();
const bs = widget.getBorderStyle();
const captions = ap?.getCaptions();
const normalText = captions?.normal ?? '';
const downText = captions?.down ?? normalText ?? '';
const borderWidth = bs?.getWidth() ?? 0;
const rotation = reduceRotation(ap?.getRotation());
const { width, height } = adjustDimsForRotation(rectangle, rotation);
const rotate = rotateInPlace({ ...rectangle, rotation });
const black = rgb(0, 0, 0);
const borderColor = componentsToColor(ap?.getBorderColor());
const normalBackgroundColor = componentsToColor(ap?.getBackgroundColor());
const downBackgroundColor = componentsToColor(ap?.getBackgroundColor(), 0.8);
const bounds = {
x: borderWidth,
y: borderWidth,
width: width - borderWidth * 2,
height: height - borderWidth * 2,
};
const normalLayout = layoutSinglelineText(normalText, {
alignment: TextAlignment.Center,
fontSize: widgetFontSize ?? fieldFontSize,
font,
bounds,
});
const downLayout = layoutSinglelineText(downText, {
alignment: TextAlignment.Center,
fontSize: widgetFontSize ?? fieldFontSize,
font,
bounds,
});
// Update font size and color
const fontSize = Math.min(normalLayout.fontSize, downLayout.fontSize);
const textColor = widgetColor ?? fieldColor ?? black;
if (widgetColor || widgetFontSize !== undefined) {
updateDefaultAppearance(widget, textColor, font, fontSize);
} else {
updateDefaultAppearance(button.acroField, textColor, font, fontSize);
}
const options = {
x: 0 + borderWidth / 2,
y: 0 + borderWidth / 2,
width: width - borderWidth,
height: height - borderWidth,
borderWidth,
borderColor,
textColor,
font: font.name,
fontSize,
};
return {
normal: [
...rotate,
...drawButton({
...options,
color: normalBackgroundColor,
textLines: [normalLayout.line],
}),
],
down: [
...rotate,
...drawButton({
...options,
color: downBackgroundColor,
textLines: [downLayout.line],
}),
],
};
};
export const defaultTextFieldAppearanceProvider: AppearanceProviderFor<PDFTextField> = (
textField,
widget,
font,
) => {
// The `/DA` entry can be at the widget or field level - so we handle both
const widgetColor = getDefaultColor(widget);
const fieldColor = getDefaultColor(textField.acroField);
const widgetFontSize = getDefaultFontSize(widget);
const fieldFontSize = getDefaultFontSize(textField.acroField);
const rectangle = widget.getRectangle();
const ap = widget.getAppearanceCharacteristics();
const bs = widget.getBorderStyle();
const text = textField.getText() ?? '';
const borderWidth = bs?.getWidth() ?? 0;
const rotation = reduceRotation(ap?.getRotation());
const { width, height } = adjustDimsForRotation(rectangle, rotation);
const rotate = rotateInPlace({ ...rectangle, rotation });
const black = rgb(0, 0, 0);
const borderColor = componentsToColor(ap?.getBorderColor());
const normalBackgroundColor = componentsToColor(ap?.getBackgroundColor());
let textLines: TextPosition[];
let fontSize: number;
const padding = textField.isCombed() ? 0 : 1;
const bounds = {
x: borderWidth + padding,
y: borderWidth + padding,
width: width - (borderWidth + padding) * 2,
height: height - (borderWidth + padding) * 2,
};
if (textField.isMultiline()) {
const layout = layoutMultilineText(text, {
alignment: textField.getAlignment(),
fontSize: widgetFontSize ?? fieldFontSize,
font,
bounds,
});
textLines = layout.lines;
fontSize = layout.fontSize;
} else if (textField.isCombed()) {
const layout = layoutCombedText(text, {
fontSize: widgetFontSize ?? fieldFontSize,
font,
bounds,
cellCount: textField.getMaxLength() ?? 0,
});
textLines = layout.cells;
fontSize = layout.fontSize;
} else {
const layout = layoutSinglelineText(text, {
alignment: textField.getAlignment(),
fontSize: widgetFontSize ?? fieldFontSize,
font,
bounds,
});
textLines = [layout.line];
fontSize = layout.fontSize;
}
// Update font size and color
const textColor = widgetColor ?? fieldColor ?? black;
if (widgetColor || widgetFontSize !== undefined) {
updateDefaultAppearance(widget, textColor, font, fontSize);
} else {
updateDefaultAppearance(textField.acroField, textColor, font, fontSize);
}
const options = {
x: 0 + borderWidth / 2,
y: 0 + borderWidth / 2,
width: width - borderWidth,
height: height - borderWidth,
borderWidth: borderWidth ?? 0,
borderColor,
textColor,
font: font.name,
fontSize,
color: normalBackgroundColor,
textLines,
padding,
};
return [...rotate, ...drawTextField(options)];
};
export const defaultDropdownAppearanceProvider: AppearanceProviderFor<PDFDropdown> = (
dropdown,
widget,
font,
) => {
// The `/DA` entry can be at the widget or field level - so we handle both
const widgetColor = getDefaultColor(widget);
const fieldColor = getDefaultColor(dropdown.acroField);
const widgetFontSize = getDefaultFontSize(widget);
const fieldFontSize = getDefaultFontSize(dropdown.acroField);
const rectangle = widget.getRectangle();
const ap = widget.getAppearanceCharacteristics();
const bs = widget.getBorderStyle();
const text = dropdown.getSelected()[0] ?? '';
const borderWidth = bs?.getWidth() ?? 0;
const rotation = reduceRotation(ap?.getRotation());
const { width, height } = adjustDimsForRotation(rectangle, rotation);
const rotate = rotateInPlace({ ...rectangle, rotation });
const black = rgb(0, 0, 0);
const borderColor = componentsToColor(ap?.getBorderColor());
const normalBackgroundColor = componentsToColor(ap?.getBackgroundColor());
const padding = 1;
const bounds = {
x: borderWidth + padding,
y: borderWidth + padding,
width: width - (borderWidth + padding) * 2,
height: height - (borderWidth + padding) * 2,
};
const { line, fontSize } = layoutSinglelineText(text, {
alignment: TextAlignment.Left,
fontSize: widgetFontSize ?? fieldFontSize,
font,
bounds,
});
// Update font size and color
const textColor = widgetColor ?? fieldColor ?? black;
if (widgetColor || widgetFontSize !== undefined) {
updateDefaultAppearance(widget, textColor, font, fontSize);
} else {
updateDefaultAppearance(dropdown.acroField, textColor, font, fontSize);
}
const options = {
x: 0 + borderWidth / 2,
y: 0 + borderWidth / 2,
width: width - borderWidth,
height: height - borderWidth,
borderWidth: borderWidth ?? 0,
borderColor,
textColor,
font: font.name,
fontSize,
color: normalBackgroundColor,
textLines: [line],
padding,
};
return [...rotate, ...drawTextField(options)];
};
export const defaultOptionListAppearanceProvider: AppearanceProviderFor<PDFOptionList> = (
optionList,
widget,
font,
) => {
// The `/DA` entry can be at the widget or field level - so we handle both
const widgetColor = getDefaultColor(widget);
const fieldColor = getDefaultColor(optionList.acroField);
const widgetFontSize = getDefaultFontSize(widget);
const fieldFontSize = getDefaultFontSize(optionList.acroField);
const rectangle = widget.getRectangle();
const ap = widget.getAppearanceCharacteristics();
const bs = widget.getBorderStyle();
const borderWidth = bs?.getWidth() ?? 0;
const rotation = reduceRotation(ap?.getRotation());
const { width, height } = adjustDimsForRotation(rectangle, rotation);
const rotate = rotateInPlace({ ...rectangle, rotation });
const black = rgb(0, 0, 0);
const borderColor = componentsToColor(ap?.getBorderColor());
const normalBackgroundColor = componentsToColor(ap?.getBackgroundColor());
const options = optionList.getOptions();
const selected = optionList.getSelected();
if (optionList.isSorted()) options.sort();
let text = '';
for (let idx = 0, len = options.length; idx < len; idx++) {
text += options[idx];
if (idx < len - 1) text += '\n';
}
const padding = 1;
const bounds = {
x: borderWidth + padding,
y: borderWidth + padding,
width: width - (borderWidth + padding) * 2,
height: height - (borderWidth + padding) * 2,
};
const { lines, fontSize, lineHeight } = layoutMultilineText(text, {
alignment: TextAlignment.Left,
fontSize: widgetFontSize ?? fieldFontSize,
font,
bounds,
});
const selectedLines: number[] = [];
for (let idx = 0, len = lines.length; idx < len; idx++) {
const line = lines[idx];
if (selected.includes(line.text)) selectedLines.push(idx);
}
const blue = rgb(153 / 255, 193 / 255, 218 / 255);
// Update font size and color
const textColor = widgetColor ?? fieldColor ?? black;
if (widgetColor || widgetFontSize !== undefined) {
updateDefaultAppearance(widget, textColor, font, fontSize);
} else {
updateDefaultAppearance(optionList.acroField, textColor, font, fontSize);
}
return [
...rotate,
...drawOptionList({
x: 0 + borderWidth / 2,
y: 0 + borderWidth / 2,
width: width - borderWidth,
height: height - borderWidth,
borderWidth: borderWidth ?? 0,
borderColor,
textColor,
font: font.name,
fontSize,
color: normalBackgroundColor,
textLines: lines,
lineHeight,
selectedColor: blue,
selectedLines,
padding,
}),
];
};

10
node_modules/pdf-lib/src/api/form/index.ts generated vendored Normal file
View File

@@ -0,0 +1,10 @@
export * from 'src/api/form/appearances';
export { default as PDFButton } from 'src/api/form/PDFButton';
export { default as PDFCheckBox } from 'src/api/form/PDFCheckBox';
export { default as PDFDropdown } from 'src/api/form/PDFDropdown';
export { default as PDFField } from 'src/api/form/PDFField';
export { default as PDFForm } from 'src/api/form/PDFForm';
export { default as PDFOptionList } from 'src/api/form/PDFOptionList';
export { default as PDFRadioGroup } from 'src/api/form/PDFRadioGroup';
export { default as PDFSignature } from 'src/api/form/PDFSignature';
export { default as PDFTextField } from 'src/api/form/PDFTextField';

5
node_modules/pdf-lib/src/api/image/alignment.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
export enum ImageAlignment {
Left = 0,
Center = 1,
Right = 2,
}

1
node_modules/pdf-lib/src/api/image/index.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export * from 'src/api/image/alignment';

20
node_modules/pdf-lib/src/api/index.ts generated vendored Normal file
View File

@@ -0,0 +1,20 @@
export * from 'src/api/form';
export * from 'src/api/text';
export * from 'src/api/colors';
export * from 'src/api/errors';
export * from 'src/api/image';
export * from 'src/api/objects';
export * from 'src/api/operations';
export * from 'src/api/operators';
export * from 'src/api/rotations';
export * from 'src/api/sizes';
export * from 'src/api/PDFPageOptions';
export * from 'src/api/PDFDocumentOptions';
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 PDFPage } from 'src/api/PDFPage';
export { default as PDFEmbeddedPage } from 'src/api/PDFEmbeddedPage';
export { default as PDFJavaScript } from 'src/api/PDFJavaScript';
export { default as Embeddable } from 'src/api/Embeddable';

10
node_modules/pdf-lib/src/api/objects.ts generated vendored Normal file
View File

@@ -0,0 +1,10 @@
import { PDFName, PDFNumber } from 'src/core';
export const asPDFName = (name: string | PDFName) =>
name instanceof PDFName ? name : PDFName.of(name);
export const asPDFNumber = (num: number | PDFNumber) =>
num instanceof PDFNumber ? num : PDFNumber.of(num);
export const asNumber = (num: number | PDFNumber) =>
num instanceof PDFNumber ? num.asNumber() : num;

800
node_modules/pdf-lib/src/api/operations.ts generated vendored Normal file
View File

@@ -0,0 +1,800 @@
import { Color, setFillingColor, setStrokingColor } from 'src/api/colors';
import {
beginText,
closePath,
drawObject,
endText,
fill,
fillAndStroke,
lineTo,
moveTo,
nextLine,
popGraphicsState,
pushGraphicsState,
rotateAndSkewTextRadiansAndTranslate,
rotateRadians,
scale,
setFontAndSize,
setLineHeight,
setLineWidth,
showText,
skewRadians,
stroke,
translate,
LineCapStyle,
setLineCap,
rotateDegrees,
setGraphicsState,
setDashPattern,
beginMarkedContent,
endMarkedContent,
clip,
endPath,
appendBezierCurve,
} from 'src/api/operators';
import { Rotation, degrees, toRadians } from 'src/api/rotations';
import { svgPathToOperators } from 'src/api/svgPath';
import { PDFHexString, PDFName, PDFNumber, PDFOperator } from 'src/core';
import { asNumber } from 'src/api/objects';
export interface DrawTextOptions {
color: Color;
font: string | PDFName;
size: number | PDFNumber;
rotate: Rotation;
xSkew: Rotation;
ySkew: Rotation;
x: number | PDFNumber;
y: number | PDFNumber;
graphicsState?: string | PDFName;
}
export const drawText = (
line: PDFHexString,
options: DrawTextOptions,
): PDFOperator[] =>
[
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
beginText(),
setFillingColor(options.color),
setFontAndSize(options.font, options.size),
rotateAndSkewTextRadiansAndTranslate(
toRadians(options.rotate),
toRadians(options.xSkew),
toRadians(options.ySkew),
options.x,
options.y,
),
showText(line),
endText(),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
export interface DrawLinesOfTextOptions extends DrawTextOptions {
lineHeight: number | PDFNumber;
}
export const drawLinesOfText = (
lines: PDFHexString[],
options: DrawLinesOfTextOptions,
): PDFOperator[] => {
const operators = [
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
beginText(),
setFillingColor(options.color),
setFontAndSize(options.font, options.size),
setLineHeight(options.lineHeight),
rotateAndSkewTextRadiansAndTranslate(
toRadians(options.rotate),
toRadians(options.xSkew),
toRadians(options.ySkew),
options.x,
options.y,
),
].filter(Boolean) as PDFOperator[];
for (let idx = 0, len = lines.length; idx < len; idx++) {
operators.push(showText(lines[idx]), nextLine());
}
operators.push(endText(), popGraphicsState());
return operators;
};
export const drawImage = (
name: string | PDFName,
options: {
x: number | PDFNumber;
y: number | PDFNumber;
width: number | PDFNumber;
height: number | PDFNumber;
rotate: Rotation;
xSkew: Rotation;
ySkew: Rotation;
graphicsState?: string | PDFName;
},
): PDFOperator[] =>
[
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
translate(options.x, options.y),
rotateRadians(toRadians(options.rotate)),
scale(options.width, options.height),
skewRadians(toRadians(options.xSkew), toRadians(options.ySkew)),
drawObject(name),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
export const drawPage = (
name: string | PDFName,
options: {
x: number | PDFNumber;
y: number | PDFNumber;
xScale: number | PDFNumber;
yScale: number | PDFNumber;
rotate: Rotation;
xSkew: Rotation;
ySkew: Rotation;
graphicsState?: string | PDFName;
},
): PDFOperator[] =>
[
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
translate(options.x, options.y),
rotateRadians(toRadians(options.rotate)),
scale(options.xScale, options.yScale),
skewRadians(toRadians(options.xSkew), toRadians(options.ySkew)),
drawObject(name),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
export const drawLine = (options: {
start: { x: number | PDFNumber; y: number | PDFNumber };
end: { x: number | PDFNumber; y: number | PDFNumber };
thickness: number | PDFNumber;
color: Color | undefined;
dashArray?: (number | PDFNumber)[];
dashPhase?: number | PDFNumber;
lineCap?: LineCapStyle;
graphicsState?: string | PDFName;
}) =>
[
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
options.color && setStrokingColor(options.color),
setLineWidth(options.thickness),
setDashPattern(options.dashArray ?? [], options.dashPhase ?? 0),
moveTo(options.start.x, options.start.y),
options.lineCap && setLineCap(options.lineCap),
moveTo(options.start.x, options.start.y),
lineTo(options.end.x, options.end.y),
stroke(),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
export const drawRectangle = (options: {
x: number | PDFNumber;
y: number | PDFNumber;
width: number | PDFNumber;
height: number | PDFNumber;
borderWidth: number | PDFNumber;
color: Color | undefined;
borderColor: Color | undefined;
rotate: Rotation;
xSkew: Rotation;
ySkew: Rotation;
borderLineCap?: LineCapStyle;
borderDashArray?: (number | PDFNumber)[];
borderDashPhase?: number | PDFNumber;
graphicsState?: string | PDFName;
}) =>
[
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
options.color && setFillingColor(options.color),
options.borderColor && setStrokingColor(options.borderColor),
setLineWidth(options.borderWidth),
options.borderLineCap && setLineCap(options.borderLineCap),
setDashPattern(options.borderDashArray ?? [], options.borderDashPhase ?? 0),
translate(options.x, options.y),
rotateRadians(toRadians(options.rotate)),
skewRadians(toRadians(options.xSkew), toRadians(options.ySkew)),
moveTo(0, 0),
lineTo(0, options.height),
lineTo(options.width, options.height),
lineTo(options.width, 0),
closePath(),
// prettier-ignore
options.color && options.borderWidth ? fillAndStroke()
: options.color ? fill()
: options.borderColor ? stroke()
: closePath(),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
const KAPPA = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0);
/** @deprecated */
export const drawEllipsePath = (config: {
x: number | PDFNumber;
y: number | PDFNumber;
xScale: number | PDFNumber;
yScale: number | PDFNumber;
}): PDFOperator[] => {
let x = asNumber(config.x);
let y = asNumber(config.y);
const xScale = asNumber(config.xScale);
const yScale = asNumber(config.yScale);
x -= xScale;
y -= yScale;
const ox = xScale * KAPPA;
const oy = yScale * KAPPA;
const xe = x + xScale * 2;
const ye = y + yScale * 2;
const xm = x + xScale;
const ym = y + yScale;
return [
pushGraphicsState(),
moveTo(x, ym),
appendBezierCurve(x, ym - oy, xm - ox, y, xm, y),
appendBezierCurve(xm + ox, y, xe, ym - oy, xe, ym),
appendBezierCurve(xe, ym + oy, xm + ox, ye, xm, ye),
appendBezierCurve(xm - ox, ye, x, ym + oy, x, ym),
popGraphicsState(),
];
};
const drawEllipseCurves = (config: {
x: number | PDFNumber;
y: number | PDFNumber;
xScale: number | PDFNumber;
yScale: number | PDFNumber;
rotate: Rotation;
}): PDFOperator[] => {
const centerX = asNumber(config.x);
const centerY = asNumber(config.y);
const xScale = asNumber(config.xScale);
const yScale = asNumber(config.yScale);
const x = -xScale;
const y = -yScale;
const ox = xScale * KAPPA;
const oy = yScale * KAPPA;
const xe = x + xScale * 2;
const ye = y + yScale * 2;
const xm = x + xScale;
const ym = y + yScale;
return [
translate(centerX, centerY),
rotateRadians(toRadians(config.rotate)),
moveTo(x, ym),
appendBezierCurve(x, ym - oy, xm - ox, y, xm, y),
appendBezierCurve(xm + ox, y, xe, ym - oy, xe, ym),
appendBezierCurve(xe, ym + oy, xm + ox, ye, xm, ye),
appendBezierCurve(xm - ox, ye, x, ym + oy, x, ym),
];
};
export const drawEllipse = (options: {
x: number | PDFNumber;
y: number | PDFNumber;
xScale: number | PDFNumber;
yScale: number | PDFNumber;
rotate?: Rotation;
color: Color | undefined;
borderColor: Color | undefined;
borderWidth: number | PDFNumber;
borderDashArray?: (number | PDFNumber)[];
borderDashPhase?: number | PDFNumber;
graphicsState?: string | PDFName;
borderLineCap?: LineCapStyle;
}) =>
[
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
options.color && setFillingColor(options.color),
options.borderColor && setStrokingColor(options.borderColor),
setLineWidth(options.borderWidth),
options.borderLineCap && setLineCap(options.borderLineCap),
setDashPattern(options.borderDashArray ?? [], options.borderDashPhase ?? 0),
// The `drawEllipsePath` branch is only here for backwards compatibility.
// See https://github.com/Hopding/pdf-lib/pull/511#issuecomment-667685655.
...(options.rotate === undefined
? drawEllipsePath({
x: options.x,
y: options.y,
xScale: options.xScale,
yScale: options.yScale,
})
: drawEllipseCurves({
x: options.x,
y: options.y,
xScale: options.xScale,
yScale: options.yScale,
rotate: options.rotate ?? degrees(0),
})),
// prettier-ignore
options.color && options.borderWidth ? fillAndStroke()
: options.color ? fill()
: options.borderColor ? stroke()
: closePath(),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
export const drawSvgPath = (
path: string,
options: {
x: number | PDFNumber;
y: number | PDFNumber;
rotate?: Rotation;
scale: number | PDFNumber | undefined;
color: Color | undefined;
borderColor: Color | undefined;
borderWidth: number | PDFNumber;
borderDashArray?: (number | PDFNumber)[];
borderDashPhase?: number | PDFNumber;
borderLineCap?: LineCapStyle;
graphicsState?: string | PDFName;
},
) =>
[
pushGraphicsState(),
options.graphicsState && setGraphicsState(options.graphicsState),
translate(options.x, options.y),
rotateRadians(toRadians(options.rotate ?? degrees(0))),
// SVG path Y axis is opposite pdf-lib's
options.scale ? scale(options.scale, -options.scale) : scale(1, -1),
options.color && setFillingColor(options.color),
options.borderColor && setStrokingColor(options.borderColor),
options.borderWidth && setLineWidth(options.borderWidth),
options.borderLineCap && setLineCap(options.borderLineCap),
setDashPattern(options.borderDashArray ?? [], options.borderDashPhase ?? 0),
...svgPathToOperators(path),
// prettier-ignore
options.color && options.borderWidth ? fillAndStroke()
: options.color ? fill()
: options.borderColor ? stroke()
: closePath(),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
export const drawCheckMark = (options: {
x: number | PDFNumber;
y: number | PDFNumber;
size: number | PDFNumber;
thickness: number | PDFNumber;
color: Color | undefined;
}) => {
const size = asNumber(options.size);
/*********************** Define Check Mark Points ***************************/
// A check mark is defined by three points in some coordinate space. Here, we
// define these points in a unit coordinate system, where the range of the x
// and y axis are both [-1, 1].
//
// Note that we do not hard code `p1y` in case we wish to change the
// size/shape of the check mark in the future. We want the check mark to
// always form a right angle. This means that the dot product between (p1-p2)
// and (p3-p2) should be zero:
//
// (p1x-p2x) * (p3x-p2x) + (p1y-p2y) * (p3y-p2y) = 0
//
// We can now rejigger this equation to solve for `p1y`:
//
// (p1y-p2y) * (p3y-p2y) = -((p1x-p2x) * (p3x-p2x))
// (p1y-p2y) = -((p1x-p2x) * (p3x-p2x)) / (p3y-p2y)
// p1y = -((p1x-p2x) * (p3x-p2x)) / (p3y-p2y) + p2y
//
// Thanks to my friend Joel Walker (https://github.com/JWalker1995) for
// devising the above equation and unit coordinate system approach!
// (x, y) coords of the check mark's bottommost point
const p2x = -1 + 0.75;
const p2y = -1 + 0.51;
// (x, y) coords of the check mark's topmost point
const p3y = 1 - 0.525;
const p3x = 1 - 0.31;
// (x, y) coords of the check mark's center (vertically) point
const p1x = -1 + 0.325;
const p1y = -((p1x - p2x) * (p3x - p2x)) / (p3y - p2y) + p2y;
/****************************************************************************/
return [
pushGraphicsState(),
options.color && setStrokingColor(options.color),
setLineWidth(options.thickness),
translate(options.x, options.y),
moveTo(p1x * size, p1y * size),
lineTo(p2x * size, p2y * size),
lineTo(p3x * size, p3y * size),
stroke(),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];
};
// prettier-ignore
export const rotateInPlace = (options: {
width: number | PDFNumber;
height: number | PDFNumber;
rotation: 0 | 90 | 180 | 270;
}) =>
options.rotation === 0 ? [
translate(0, 0),
rotateDegrees(0)
]
: options.rotation === 90 ? [
translate(options.width, 0),
rotateDegrees(90)
]
: options.rotation === 180 ? [
translate(options.width, options.height),
rotateDegrees(180)
]
: options.rotation === 270 ? [
translate(0, options.height),
rotateDegrees(270)
]
: []; // Invalid rotation - noop
export const drawCheckBox = (options: {
x: number | PDFNumber;
y: number | PDFNumber;
width: number | PDFNumber;
height: number | PDFNumber;
thickness: number | PDFNumber;
borderWidth: number | PDFNumber;
markColor: Color | undefined;
color: Color | undefined;
borderColor: Color | undefined;
filled: boolean;
}) => {
const outline = drawRectangle({
x: options.x,
y: options.y,
width: options.width,
height: options.height,
borderWidth: options.borderWidth,
color: options.color,
borderColor: options.borderColor,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
if (!options.filled) return outline;
const width = asNumber(options.width);
const height = asNumber(options.height);
const checkMarkSize = Math.min(width, height) / 2;
const checkMark = drawCheckMark({
x: width / 2,
y: height / 2,
size: checkMarkSize,
thickness: options.thickness,
color: options.markColor,
});
return [pushGraphicsState(), ...outline, ...checkMark, popGraphicsState()];
};
export const drawRadioButton = (options: {
x: number | PDFNumber;
y: number | PDFNumber;
width: number | PDFNumber;
height: number | PDFNumber;
borderWidth: number | PDFNumber;
dotColor: Color | undefined;
color: Color | undefined;
borderColor: Color | undefined;
filled: boolean;
}) => {
const width = asNumber(options.width);
const height = asNumber(options.height);
const outlineScale = Math.min(width, height) / 2;
const outline = drawEllipse({
x: options.x,
y: options.y,
xScale: outlineScale,
yScale: outlineScale,
color: options.color,
borderColor: options.borderColor,
borderWidth: options.borderWidth,
});
if (!options.filled) return outline;
const dot = drawEllipse({
x: options.x,
y: options.y,
xScale: outlineScale * 0.45,
yScale: outlineScale * 0.45,
color: options.dotColor,
borderColor: undefined,
borderWidth: 0,
});
return [pushGraphicsState(), ...outline, ...dot, popGraphicsState()];
};
export const drawButton = (options: {
x: number | PDFNumber;
y: number | PDFNumber;
width: number | PDFNumber;
height: number | PDFNumber;
borderWidth: number | PDFNumber;
color: Color | undefined;
borderColor: Color | undefined;
textLines: { encoded: PDFHexString; x: number; y: number }[];
textColor: Color;
font: string | PDFName;
fontSize: number | PDFNumber;
}) => {
const x = asNumber(options.x);
const y = asNumber(options.y);
const width = asNumber(options.width);
const height = asNumber(options.height);
const background = drawRectangle({
x,
y,
width,
height,
borderWidth: options.borderWidth,
color: options.color,
borderColor: options.borderColor,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const lines = drawTextLines(options.textLines, {
color: options.textColor,
font: options.font,
size: options.fontSize,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
return [pushGraphicsState(), ...background, ...lines, popGraphicsState()];
};
export interface DrawTextLinesOptions {
color: Color;
font: string | PDFName;
size: number | PDFNumber;
rotate: Rotation;
xSkew: Rotation;
ySkew: Rotation;
}
export const drawTextLines = (
lines: { encoded: PDFHexString; x: number; y: number }[],
options: DrawTextLinesOptions,
): PDFOperator[] => {
const operators = [
beginText(),
setFillingColor(options.color),
setFontAndSize(options.font, options.size),
];
for (let idx = 0, len = lines.length; idx < len; idx++) {
const { encoded, x, y } = lines[idx];
operators.push(
rotateAndSkewTextRadiansAndTranslate(
toRadians(options.rotate),
toRadians(options.xSkew),
toRadians(options.ySkew),
x,
y,
),
showText(encoded),
);
}
operators.push(endText());
return operators;
};
export const drawTextField = (options: {
x: number | PDFNumber;
y: number | PDFNumber;
width: number | PDFNumber;
height: number | PDFNumber;
borderWidth: number | PDFNumber;
color: Color | undefined;
borderColor: Color | undefined;
textLines: { encoded: PDFHexString; x: number; y: number }[];
textColor: Color;
font: string | PDFName;
fontSize: number | PDFNumber;
padding: number | PDFNumber;
}) => {
const x = asNumber(options.x);
const y = asNumber(options.y);
const width = asNumber(options.width);
const height = asNumber(options.height);
const borderWidth = asNumber(options.borderWidth);
const padding = asNumber(options.padding);
const clipX = x + borderWidth / 2 + padding;
const clipY = y + borderWidth / 2 + padding;
const clipWidth = width - (borderWidth / 2 + padding) * 2;
const clipHeight = height - (borderWidth / 2 + padding) * 2;
const clippingArea = [
moveTo(clipX, clipY),
lineTo(clipX, clipY + clipHeight),
lineTo(clipX + clipWidth, clipY + clipHeight),
lineTo(clipX + clipWidth, clipY),
closePath(),
clip(),
endPath(),
];
const background = drawRectangle({
x,
y,
width,
height,
borderWidth: options.borderWidth,
color: options.color,
borderColor: options.borderColor,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const lines = drawTextLines(options.textLines, {
color: options.textColor,
font: options.font,
size: options.fontSize,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const markedContent = [
beginMarkedContent('Tx'),
pushGraphicsState(),
...lines,
popGraphicsState(),
endMarkedContent(),
];
return [
pushGraphicsState(),
...background,
...clippingArea,
...markedContent,
popGraphicsState(),
];
};
export const drawOptionList = (options: {
x: number | PDFNumber;
y: number | PDFNumber;
width: number | PDFNumber;
height: number | PDFNumber;
borderWidth: number | PDFNumber;
color: Color | undefined;
borderColor: Color | undefined;
textLines: { encoded: PDFHexString; x: number; y: number; height: number }[];
textColor: Color;
font: string | PDFName;
fontSize: number | PDFNumber;
lineHeight: number | PDFNumber;
selectedLines: number[];
selectedColor: Color;
padding: number | PDFNumber;
}) => {
const x = asNumber(options.x);
const y = asNumber(options.y);
const width = asNumber(options.width);
const height = asNumber(options.height);
const lineHeight = asNumber(options.lineHeight);
const borderWidth = asNumber(options.borderWidth);
const padding = asNumber(options.padding);
const clipX = x + borderWidth / 2 + padding;
const clipY = y + borderWidth / 2 + padding;
const clipWidth = width - (borderWidth / 2 + padding) * 2;
const clipHeight = height - (borderWidth / 2 + padding) * 2;
const clippingArea = [
moveTo(clipX, clipY),
lineTo(clipX, clipY + clipHeight),
lineTo(clipX + clipWidth, clipY + clipHeight),
lineTo(clipX + clipWidth, clipY),
closePath(),
clip(),
endPath(),
];
const background = drawRectangle({
x,
y,
width,
height,
borderWidth: options.borderWidth,
color: options.color,
borderColor: options.borderColor,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const highlights: PDFOperator[] = [];
for (let idx = 0, len = options.selectedLines.length; idx < len; idx++) {
const line = options.textLines[options.selectedLines[idx]];
highlights.push(
...drawRectangle({
x: line.x - padding,
y: line.y - (lineHeight - line.height) / 2,
width: width - borderWidth,
height: line.height + (lineHeight - line.height) / 2,
borderWidth: 0,
color: options.selectedColor,
borderColor: undefined,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
}),
);
}
const lines = drawTextLines(options.textLines, {
color: options.textColor,
font: options.font,
size: options.fontSize,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
});
const markedContent = [
beginMarkedContent('Tx'),
pushGraphicsState(),
...lines,
popGraphicsState(),
endMarkedContent(),
];
return [
pushGraphicsState(),
...background,
...highlights,
...clippingArea,
...markedContent,
popGraphicsState(),
];
};

360
node_modules/pdf-lib/src/api/operators.ts generated vendored Normal file
View File

@@ -0,0 +1,360 @@
import { asNumber, asPDFName, asPDFNumber } from 'src/api/objects';
import { degreesToRadians } from 'src/api/rotations';
import {
PDFHexString,
PDFName,
PDFNumber,
PDFOperator,
PDFOperatorNames as Ops,
} from 'src/core';
/* ==================== Clipping Path Operators ==================== */
export const clip = () => PDFOperator.of(Ops.ClipNonZero);
export const clipEvenOdd = () => PDFOperator.of(Ops.ClipEvenOdd);
/* ==================== Graphics State Operators ==================== */
const { cos, sin, tan } = Math;
export const concatTransformationMatrix = (
a: number | PDFNumber,
b: number | PDFNumber,
c: number | PDFNumber,
d: number | PDFNumber,
e: number | PDFNumber,
f: number | PDFNumber,
) =>
PDFOperator.of(Ops.ConcatTransformationMatrix, [
asPDFNumber(a),
asPDFNumber(b),
asPDFNumber(c),
asPDFNumber(d),
asPDFNumber(e),
asPDFNumber(f),
]);
export const translate = (xPos: number | PDFNumber, yPos: number | PDFNumber) =>
concatTransformationMatrix(1, 0, 0, 1, xPos, yPos);
export const scale = (xPos: number | PDFNumber, yPos: number | PDFNumber) =>
concatTransformationMatrix(xPos, 0, 0, yPos, 0, 0);
export const rotateRadians = (angle: number | PDFNumber) =>
concatTransformationMatrix(
cos(asNumber(angle)),
sin(asNumber(angle)),
-sin(asNumber(angle)),
cos(asNumber(angle)),
0,
0,
);
export const rotateDegrees = (angle: number | PDFNumber) =>
rotateRadians(degreesToRadians(asNumber(angle)));
export const skewRadians = (
xSkewAngle: number | PDFNumber,
ySkewAngle: number | PDFNumber,
) =>
concatTransformationMatrix(
1,
tan(asNumber(xSkewAngle)),
tan(asNumber(ySkewAngle)),
1,
0,
0,
);
export const skewDegrees = (
xSkewAngle: number | PDFNumber,
ySkewAngle: number | PDFNumber,
) =>
skewRadians(
degreesToRadians(asNumber(xSkewAngle)),
degreesToRadians(asNumber(ySkewAngle)),
);
export const setDashPattern = (
dashArray: (number | PDFNumber)[],
dashPhase: number | PDFNumber,
) =>
PDFOperator.of(Ops.SetLineDashPattern, [
`[${dashArray.map(asPDFNumber).join(' ')}]`,
asPDFNumber(dashPhase),
]);
export const restoreDashPattern = () => setDashPattern([], 0);
export enum LineCapStyle {
Butt = 0,
Round = 1,
Projecting = 2,
}
export const setLineCap = (style: LineCapStyle) =>
PDFOperator.of(Ops.SetLineCapStyle, [asPDFNumber(style)]);
export enum LineJoinStyle {
Miter = 0,
Round = 1,
Bevel = 2,
}
export const setLineJoin = (style: LineJoinStyle) =>
PDFOperator.of(Ops.SetLineJoinStyle, [asPDFNumber(style)]);
export const setGraphicsState = (state: string | PDFName) =>
PDFOperator.of(Ops.SetGraphicsStateParams, [asPDFName(state)]);
export const pushGraphicsState = () => PDFOperator.of(Ops.PushGraphicsState);
export const popGraphicsState = () => PDFOperator.of(Ops.PopGraphicsState);
export const setLineWidth = (width: number | PDFNumber) =>
PDFOperator.of(Ops.SetLineWidth, [asPDFNumber(width)]);
/* ==================== Path Construction Operators ==================== */
export const appendBezierCurve = (
x1: number | PDFNumber,
y1: number | PDFNumber,
x2: number | PDFNumber,
y2: number | PDFNumber,
x3: number | PDFNumber,
y3: number | PDFNumber,
) =>
PDFOperator.of(Ops.AppendBezierCurve, [
asPDFNumber(x1),
asPDFNumber(y1),
asPDFNumber(x2),
asPDFNumber(y2),
asPDFNumber(x3),
asPDFNumber(y3),
]);
export const appendQuadraticCurve = (
x1: number | PDFNumber,
y1: number | PDFNumber,
x2: number | PDFNumber,
y2: number | PDFNumber,
) =>
PDFOperator.of(Ops.CurveToReplicateInitialPoint, [
asPDFNumber(x1),
asPDFNumber(y1),
asPDFNumber(x2),
asPDFNumber(y2),
]);
export const closePath = () => PDFOperator.of(Ops.ClosePath);
export const moveTo = (xPos: number | PDFNumber, yPos: number | PDFNumber) =>
PDFOperator.of(Ops.MoveTo, [asPDFNumber(xPos), asPDFNumber(yPos)]);
export const lineTo = (xPos: number | PDFNumber, yPos: number | PDFNumber) =>
PDFOperator.of(Ops.LineTo, [asPDFNumber(xPos), asPDFNumber(yPos)]);
/**
* @param xPos x coordinate for the lower left corner of the rectangle
* @param yPos y coordinate for the lower left corner of the rectangle
* @param width width of the rectangle
* @param height height of the rectangle
*/
export const rectangle = (
xPos: number | PDFNumber,
yPos: number | PDFNumber,
width: number | PDFNumber,
height: number | PDFNumber,
) =>
PDFOperator.of(Ops.AppendRectangle, [
asPDFNumber(xPos),
asPDFNumber(yPos),
asPDFNumber(width),
asPDFNumber(height),
]);
/**
* @param xPos x coordinate for the lower left corner of the square
* @param yPos y coordinate for the lower left corner of the square
* @param size width and height of the square
*/
export const square = (xPos: number, yPos: number, size: number) =>
rectangle(xPos, yPos, size, size);
/* ==================== Path Painting Operators ==================== */
export const stroke = () => PDFOperator.of(Ops.StrokePath);
export const fill = () => PDFOperator.of(Ops.FillNonZero);
export const fillAndStroke = () => PDFOperator.of(Ops.FillNonZeroAndStroke);
export const endPath = () => PDFOperator.of(Ops.EndPath);
/* ==================== Text Positioning Operators ==================== */
export const nextLine = () => PDFOperator.of(Ops.NextLine);
export const moveText = (x: number | PDFNumber, y: number | PDFNumber) =>
PDFOperator.of(Ops.MoveText, [asPDFNumber(x), asPDFNumber(y)]);
/* ==================== Text Showing Operators ==================== */
export const showText = (text: PDFHexString) =>
PDFOperator.of(Ops.ShowText, [text]);
/* ==================== Text State Operators ==================== */
export const beginText = () => PDFOperator.of(Ops.BeginText);
export const endText = () => PDFOperator.of(Ops.EndText);
export const setFontAndSize = (
name: string | PDFName,
size: number | PDFNumber,
) => PDFOperator.of(Ops.SetFontAndSize, [asPDFName(name), asPDFNumber(size)]);
export const setCharacterSpacing = (spacing: number | PDFNumber) =>
PDFOperator.of(Ops.SetCharacterSpacing, [asPDFNumber(spacing)]);
export const setWordSpacing = (spacing: number | PDFNumber) =>
PDFOperator.of(Ops.SetWordSpacing, [asPDFNumber(spacing)]);
/** @param squeeze horizontal character spacing */
export const setCharacterSqueeze = (squeeze: number | PDFNumber) =>
PDFOperator.of(Ops.SetTextHorizontalScaling, [asPDFNumber(squeeze)]);
export const setLineHeight = (lineHeight: number | PDFNumber) =>
PDFOperator.of(Ops.SetTextLineHeight, [asPDFNumber(lineHeight)]);
export const setTextRise = (rise: number | PDFNumber) =>
PDFOperator.of(Ops.SetTextRise, [asPDFNumber(rise)]);
export enum TextRenderingMode {
Fill = 0,
Outline = 1,
FillAndOutline = 2,
Invisible = 3,
FillAndClip = 4,
OutlineAndClip = 5,
FillAndOutlineAndClip = 6,
Clip = 7,
}
export const setTextRenderingMode = (mode: TextRenderingMode) =>
PDFOperator.of(Ops.SetTextRenderingMode, [asPDFNumber(mode)]);
export const setTextMatrix = (
a: number | PDFNumber,
b: number | PDFNumber,
c: number | PDFNumber,
d: number | PDFNumber,
e: number | PDFNumber,
f: number | PDFNumber,
) =>
PDFOperator.of(Ops.SetTextMatrix, [
asPDFNumber(a),
asPDFNumber(b),
asPDFNumber(c),
asPDFNumber(d),
asPDFNumber(e),
asPDFNumber(f),
]);
export const rotateAndSkewTextRadiansAndTranslate = (
rotationAngle: number | PDFNumber,
xSkewAngle: number | PDFNumber,
ySkewAngle: number | PDFNumber,
x: number | PDFNumber,
y: number | PDFNumber,
) =>
setTextMatrix(
cos(asNumber(rotationAngle)),
sin(asNumber(rotationAngle)) + tan(asNumber(xSkewAngle)),
-sin(asNumber(rotationAngle)) + tan(asNumber(ySkewAngle)),
cos(asNumber(rotationAngle)),
x,
y,
);
export const rotateAndSkewTextDegreesAndTranslate = (
rotationAngle: number | PDFNumber,
xSkewAngle: number | PDFNumber,
ySkewAngle: number | PDFNumber,
x: number | PDFNumber,
y: number | PDFNumber,
) =>
rotateAndSkewTextRadiansAndTranslate(
degreesToRadians(asNumber(rotationAngle)),
degreesToRadians(asNumber(xSkewAngle)),
degreesToRadians(asNumber(ySkewAngle)),
x,
y,
);
/* ==================== XObject Operator ==================== */
export const drawObject = (name: string | PDFName) =>
PDFOperator.of(Ops.DrawObject, [asPDFName(name)]);
/* ==================== Color Operators ==================== */
export const setFillingGrayscaleColor = (gray: number | PDFNumber) =>
PDFOperator.of(Ops.NonStrokingColorGray, [asPDFNumber(gray)]);
export const setStrokingGrayscaleColor = (gray: number | PDFNumber) =>
PDFOperator.of(Ops.StrokingColorGray, [asPDFNumber(gray)]);
export const setFillingRgbColor = (
red: number | PDFNumber,
green: number | PDFNumber,
blue: number | PDFNumber,
) =>
PDFOperator.of(Ops.NonStrokingColorRgb, [
asPDFNumber(red),
asPDFNumber(green),
asPDFNumber(blue),
]);
export const setStrokingRgbColor = (
red: number | PDFNumber,
green: number | PDFNumber,
blue: number | PDFNumber,
) =>
PDFOperator.of(Ops.StrokingColorRgb, [
asPDFNumber(red),
asPDFNumber(green),
asPDFNumber(blue),
]);
export const setFillingCmykColor = (
cyan: number | PDFNumber,
magenta: number | PDFNumber,
yellow: number | PDFNumber,
key: number | PDFNumber,
) =>
PDFOperator.of(Ops.NonStrokingColorCmyk, [
asPDFNumber(cyan),
asPDFNumber(magenta),
asPDFNumber(yellow),
asPDFNumber(key),
]);
export const setStrokingCmykColor = (
cyan: number | PDFNumber,
magenta: number | PDFNumber,
yellow: number | PDFNumber,
key: number | PDFNumber,
) =>
PDFOperator.of(Ops.StrokingColorCmyk, [
asPDFNumber(cyan),
asPDFNumber(magenta),
asPDFNumber(yellow),
asPDFNumber(key),
]);
/* ==================== Marked Content Operators ==================== */
export const beginMarkedContent = (tag: string | PDFName) =>
PDFOperator.of(Ops.BeginMarkedContent, [asPDFName(tag)]);
export const endMarkedContent = () => PDFOperator.of(Ops.EndMarkedContent);

87
node_modules/pdf-lib/src/api/rotations.ts generated vendored Normal file
View File

@@ -0,0 +1,87 @@
import { assertIs, error } from 'src/utils';
export enum RotationTypes {
Degrees = 'degrees',
Radians = 'radians',
}
export interface Radians {
type: RotationTypes.Radians;
angle: number;
}
export interface Degrees {
type: RotationTypes.Degrees;
angle: number;
}
export type Rotation = Radians | Degrees;
export const radians = (radianAngle: number): Radians => {
assertIs(radianAngle, 'radianAngle', ['number']);
return { type: RotationTypes.Radians, angle: radianAngle };
};
export const degrees = (degreeAngle: number): Degrees => {
assertIs(degreeAngle, 'degreeAngle', ['number']);
return { type: RotationTypes.Degrees, angle: degreeAngle };
};
const { Radians, Degrees } = RotationTypes;
export const degreesToRadians = (degree: number) => (degree * Math.PI) / 180;
export const radiansToDegrees = (radian: number) => (radian * 180) / Math.PI;
// prettier-ignore
export const toRadians = (rotation: Rotation) =>
rotation.type === Radians ? rotation.angle
: rotation.type === Degrees ? degreesToRadians(rotation.angle)
: error(`Invalid rotation: ${JSON.stringify(rotation)}`);
// prettier-ignore
export const toDegrees = (rotation: Rotation) =>
rotation.type === Radians ? radiansToDegrees(rotation.angle)
: rotation.type === Degrees ? rotation.angle
: error(`Invalid rotation: ${JSON.stringify(rotation)}`);
export const reduceRotation = (degreeAngle = 0) => {
const quadrants = (degreeAngle / 90) % 4;
if (quadrants === 0) return 0;
if (quadrants === 1) return 90;
if (quadrants === 2) return 180;
if (quadrants === 3) return 270;
return 0; // `degreeAngle` is not a multiple of 90
};
export const adjustDimsForRotation = (
dims: { width: number; height: number },
degreeAngle = 0,
) => {
const rotation = reduceRotation(degreeAngle);
return rotation === 90 || rotation === 270
? { width: dims.height, height: dims.width }
: { width: dims.width, height: dims.height };
};
export const rotateRectangle = (
rectangle: {
x: number;
y: number;
width: number;
height: number;
},
borderWidth = 0,
degreeAngle = 0,
) => {
const { x, y, width: w, height: h } = rectangle;
const r = reduceRotation(degreeAngle);
const b = borderWidth / 2;
// prettier-ignore
if (r === 0) return { x: x - b, y: y - b, width: w, height: h };
else if (r === 90) return { x: x - h + b, y: y - b, width: h, height: w };
else if (r === 180) return { x: x - w + b, y: y - h + b, width: w, height: h };
else if (r === 270) return { x: x - b, y: y - w + b, width: h, height: w };
else return { x: x - b, y: y - b, width: w, height: h };
};

52
node_modules/pdf-lib/src/api/sizes.ts generated vendored Normal file
View File

@@ -0,0 +1,52 @@
export const PageSizes = {
'4A0': [4767.87, 6740.79] as [number, number],
'2A0': [3370.39, 4767.87] as [number, number],
A0: [2383.94, 3370.39] as [number, number],
A1: [1683.78, 2383.94] as [number, number],
A2: [1190.55, 1683.78] as [number, number],
A3: [841.89, 1190.55] as [number, number],
A4: [595.28, 841.89] as [number, number],
A5: [419.53, 595.28] as [number, number],
A6: [297.64, 419.53] as [number, number],
A7: [209.76, 297.64] as [number, number],
A8: [147.4, 209.76] as [number, number],
A9: [104.88, 147.4] as [number, number],
A10: [73.7, 104.88] as [number, number],
B0: [2834.65, 4008.19] as [number, number],
B1: [2004.09, 2834.65] as [number, number],
B2: [1417.32, 2004.09] as [number, number],
B3: [1000.63, 1417.32] as [number, number],
B4: [708.66, 1000.63] as [number, number],
B5: [498.9, 708.66] as [number, number],
B6: [354.33, 498.9] as [number, number],
B7: [249.45, 354.33] as [number, number],
B8: [175.75, 249.45] as [number, number],
B9: [124.72, 175.75] as [number, number],
B10: [87.87, 124.72] as [number, number],
C0: [2599.37, 3676.54] as [number, number],
C1: [1836.85, 2599.37] as [number, number],
C2: [1298.27, 1836.85] as [number, number],
C3: [918.43, 1298.27] as [number, number],
C4: [649.13, 918.43] as [number, number],
C5: [459.21, 649.13] as [number, number],
C6: [323.15, 459.21] as [number, number],
C7: [229.61, 323.15] as [number, number],
C8: [161.57, 229.61] as [number, number],
C9: [113.39, 161.57] as [number, number],
C10: [79.37, 113.39] as [number, number],
RA0: [2437.8, 3458.27] as [number, number],
RA1: [1729.13, 2437.8] as [number, number],
RA2: [1218.9, 1729.13] as [number, number],
RA3: [864.57, 1218.9] as [number, number],
RA4: [609.45, 864.57] as [number, number],
SRA0: [2551.18, 3628.35] as [number, number],
SRA1: [1814.17, 2551.18] as [number, number],
SRA2: [1275.59, 1814.17] as [number, number],
SRA3: [907.09, 1275.59] as [number, number],
SRA4: [637.8, 907.09] as [number, number],
Executive: [521.86, 756.0] as [number, number],
Folio: [612.0, 936.0] as [number, number],
Legal: [612.0, 1008.0] as [number, number],
Letter: [612.0, 792.0] as [number, number],
Tabloid: [792.0, 1224.0] as [number, number],
};

489
node_modules/pdf-lib/src/api/svgPath.ts generated vendored Normal file
View File

@@ -0,0 +1,489 @@
// Originated from pdfkit Copyright (c) 2014 Devon Govett
// https://github.com/foliojs/pdfkit/blob/1e62e6ffe24b378eb890df507a47610f4c4a7b24/lib/path.js
// MIT LICENSE
// Updated for pdf-lib & TypeScript by Jeremy Messenger
import {
appendBezierCurve,
appendQuadraticCurve,
closePath,
lineTo,
moveTo,
} from 'src/api/operators';
import { PDFOperator } from 'src/core';
let cx: number = 0;
let cy: number = 0;
let px: number | null = 0;
let py: number | null = 0;
let sx: number = 0;
let sy: number = 0;
const parameters = new Map<string, number>([
['A', 7],
['a', 7],
['C', 6],
['c', 6],
['H', 1],
['h', 1],
['L', 2],
['l', 2],
['M', 2],
['m', 2],
['Q', 4],
['q', 4],
['S', 4],
['s', 4],
['T', 2],
['t', 2],
['V', 1],
['v', 1],
['Z', 0],
['z', 0],
]);
interface Cmd {
cmd?: string;
args: number[];
}
const parse = (path: string) => {
let cmd;
const ret: Cmd[] = [];
let args: number[] = [];
let curArg = '';
let foundDecimal = false;
let params = 0;
for (const c of path) {
if (parameters.has(c)) {
params = parameters.get(c)!;
if (cmd) {
// save existing command
if (curArg.length > 0) {
args[args.length] = +curArg;
}
ret[ret.length] = { cmd, args };
args = [];
curArg = '';
foundDecimal = false;
}
cmd = c;
} else if (
[' ', ','].includes(c) ||
(c === '-' && curArg.length > 0 && curArg[curArg.length - 1] !== 'e') ||
(c === '.' && foundDecimal)
) {
if (curArg.length === 0) {
continue;
}
if (args.length === params) {
// handle reused commands
ret[ret.length] = { cmd, args };
args = [+curArg];
// handle assumed commands
if (cmd === 'M') {
cmd = 'L';
}
if (cmd === 'm') {
cmd = 'l';
}
} else {
args[args.length] = +curArg;
}
foundDecimal = c === '.';
// fix for negative numbers or repeated decimals with no delimeter between commands
curArg = ['-', '.'].includes(c) ? c : '';
} else {
curArg += c;
if (c === '.') {
foundDecimal = true;
}
}
}
// add the last command
if (curArg.length > 0) {
if (args.length === params) {
// handle reused commands
ret[ret.length] = { cmd, args };
args = [+curArg];
// handle assumed commands
if (cmd === 'M') {
cmd = 'L';
}
if (cmd === 'm') {
cmd = 'l';
}
} else {
args[args.length] = +curArg;
}
}
ret[ret.length] = { cmd, args };
return ret;
};
const apply = (commands: Cmd[]) => {
// current point, control point, and subpath starting point
cx = cy = px = py = sx = sy = 0;
// run the commands
let cmds: PDFOperator[] = [];
for (let i = 0; i < commands.length; i++) {
const c = commands[i];
if (c.cmd && typeof runners[c.cmd] === 'function') {
const cmd = runners[c.cmd](c.args);
if (Array.isArray(cmd)) {
cmds = cmds.concat(cmd);
} else {
cmds.push(cmd);
}
}
}
return cmds;
};
interface CmdToOperatorsMap {
[cmd: string]: (a: number[]) => PDFOperator | PDFOperator[];
}
const runners: CmdToOperatorsMap = {
M(a) {
cx = a[0];
cy = a[1];
px = py = null;
sx = cx;
sy = cy;
return moveTo(cx, cy);
},
m(a) {
cx += a[0];
cy += a[1];
px = py = null;
sx = cx;
sy = cy;
return moveTo(cx, cy);
},
C(a) {
cx = a[4];
cy = a[5];
px = a[2];
py = a[3];
return appendBezierCurve(a[0], a[1], a[2], a[3], a[4], a[5]);
},
c(a) {
const cmd = appendBezierCurve(
a[0] + cx,
a[1] + cy,
a[2] + cx,
a[3] + cy,
a[4] + cx,
a[5] + cy,
);
px = cx + a[2];
py = cy + a[3];
cx += a[4];
cy += a[5];
return cmd;
},
S(a) {
if (px === null || py === null) {
px = cx;
py = cy;
}
const cmd = appendBezierCurve(
cx - (px - cx),
cy - (py - cy),
a[0],
a[1],
a[2],
a[3],
);
px = a[0];
py = a[1];
cx = a[2];
cy = a[3];
return cmd;
},
s(a) {
if (px === null || py === null) {
px = cx;
py = cy;
}
const cmd = appendBezierCurve(
cx - (px - cx),
cy - (py - cy),
cx + a[0],
cy + a[1],
cx + a[2],
cy + a[3],
);
px = cx + a[0];
py = cy + a[1];
cx += a[2];
cy += a[3];
return cmd;
},
Q(a) {
px = a[0];
py = a[1];
cx = a[2];
cy = a[3];
return appendQuadraticCurve(a[0], a[1], cx, cy);
},
q(a) {
const cmd = appendQuadraticCurve(
a[0] + cx,
a[1] + cy,
a[2] + cx,
a[3] + cy,
);
px = cx + a[0];
py = cy + a[1];
cx += a[2];
cy += a[3];
return cmd;
},
T(a) {
if (px === null || py === null) {
px = cx;
py = cy;
} else {
px = cx - (px - cx);
py = cy - (py - cy);
}
const cmd = appendQuadraticCurve(px, py, a[0], a[1]);
px = cx - (px - cx);
py = cy - (py - cy);
cx = a[0];
cy = a[1];
return cmd;
},
t(a) {
if (px === null || py === null) {
px = cx;
py = cy;
} else {
px = cx - (px - cx);
py = cy - (py - cy);
}
const cmd = appendQuadraticCurve(px, py, cx + a[0], cy + a[1]);
cx += a[0];
cy += a[1];
return cmd;
},
A(a) {
const cmds = solveArc(cx, cy, a);
cx = a[5];
cy = a[6];
return cmds;
},
a(a) {
a[5] += cx;
a[6] += cy;
const cmds = solveArc(cx, cy, a);
cx = a[5];
cy = a[6];
return cmds;
},
L(a) {
cx = a[0];
cy = a[1];
px = py = null;
return lineTo(cx, cy);
},
l(a) {
cx += a[0];
cy += a[1];
px = py = null;
return lineTo(cx, cy);
},
H(a) {
cx = a[0];
px = py = null;
return lineTo(cx, cy);
},
h(a) {
cx += a[0];
px = py = null;
return lineTo(cx, cy);
},
V(a) {
cy = a[0];
px = py = null;
return lineTo(cx, cy);
},
v(a) {
cy += a[0];
px = py = null;
return lineTo(cx, cy);
},
Z() {
const cmd = closePath();
cx = sx;
cy = sy;
return cmd;
},
z() {
const cmd = closePath();
cx = sx;
cy = sy;
return cmd;
},
};
const solveArc = (x: number, y: number, coords: number[]) => {
const [rx, ry, rot, large, sweep, ex, ey] = coords;
const segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y);
const cmds: PDFOperator[] = [];
for (const seg of segs) {
const bez = segmentToBezier(...seg);
cmds.push(appendBezierCurve(...bez));
}
return cmds;
};
type Segment = [number, number, number, number, number, number, number, number];
type Bezier = [number, number, number, number, number, number];
// from Inkscape svgtopdf, thanks!
const arcToSegments = (
x: number,
y: number,
rx: number,
ry: number,
large: number,
sweep: number,
rotateX: number,
ox: number,
oy: number,
) => {
const th = rotateX * (Math.PI / 180);
const sinTh = Math.sin(th);
const cosTh = Math.cos(th);
rx = Math.abs(rx);
ry = Math.abs(ry);
px = cosTh * (ox - x) * 0.5 + sinTh * (oy - y) * 0.5;
py = cosTh * (oy - y) * 0.5 - sinTh * (ox - x) * 0.5;
let pl = (px * px) / (rx * rx) + (py * py) / (ry * ry);
if (pl > 1) {
pl = Math.sqrt(pl);
rx *= pl;
ry *= pl;
}
const a00 = cosTh / rx;
const a01 = sinTh / rx;
const a10 = -sinTh / ry;
const a11 = cosTh / ry;
const x0 = a00 * ox + a01 * oy;
const y0 = a10 * ox + a11 * oy;
const x1 = a00 * x + a01 * y;
const y1 = a10 * x + a11 * y;
const d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0);
let sfactorSq = 1 / d - 0.25;
if (sfactorSq < 0) {
sfactorSq = 0;
}
let sfactor = Math.sqrt(sfactorSq);
if (sweep === large) {
sfactor = -sfactor;
}
const xc = 0.5 * (x0 + x1) - sfactor * (y1 - y0);
const yc = 0.5 * (y0 + y1) + sfactor * (x1 - x0);
const th0 = Math.atan2(y0 - yc, x0 - xc);
const th1 = Math.atan2(y1 - yc, x1 - xc);
let thArc = th1 - th0;
if (thArc < 0 && sweep === 1) {
thArc += 2 * Math.PI;
} else if (thArc > 0 && sweep === 0) {
thArc -= 2 * Math.PI;
}
const segments = Math.ceil(Math.abs(thArc / (Math.PI * 0.5 + 0.001)));
const result: Segment[] = [];
for (let i = 0; i < segments; i++) {
const th2 = th0 + (i * thArc) / segments;
const th3 = th0 + ((i + 1) * thArc) / segments;
result[i] = [xc, yc, th2, th3, rx, ry, sinTh, cosTh];
}
return result;
};
const segmentToBezier = (
cx1: number,
cy1: number,
th0: number,
th1: number,
rx: number,
ry: number,
sinTh: number,
cosTh: number,
) => {
const a00 = cosTh * rx;
const a01 = -sinTh * ry;
const a10 = sinTh * rx;
const a11 = cosTh * ry;
const thHalf = 0.5 * (th1 - th0);
const t =
((8 / 3) * Math.sin(thHalf * 0.5) * Math.sin(thHalf * 0.5)) /
Math.sin(thHalf);
const x1 = cx1 + Math.cos(th0) - t * Math.sin(th0);
const y1 = cy1 + Math.sin(th0) + t * Math.cos(th0);
const x3 = cx1 + Math.cos(th1);
const y3 = cy1 + Math.sin(th1);
const x2 = x3 + t * Math.sin(th1);
const y2 = y3 - t * Math.cos(th1);
const result: Bezier = [
a00 * x1 + a01 * y1,
a10 * x1 + a11 * y1,
a00 * x2 + a01 * y2,
a10 * x2 + a11 * y2,
a00 * x3 + a01 * y3,
a10 * x3 + a11 * y3,
];
return result;
};
export const svgPathToOperators = (path: string) => apply(parse(path));

5
node_modules/pdf-lib/src/api/text/alignment.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
export enum TextAlignment {
Left = 0,
Center = 1,
Right = 2,
}

2
node_modules/pdf-lib/src/api/text/index.ts generated vendored Normal file
View File

@@ -0,0 +1,2 @@
export * from 'src/api/text/alignment';
export * from 'src/api/text/layout';

343
node_modules/pdf-lib/src/api/text/layout.ts generated vendored Normal file
View File

@@ -0,0 +1,343 @@
import PDFFont from 'src/api/PDFFont';
import { CombedTextLayoutError } from 'src/api/errors';
import { TextAlignment } from 'src/api/text/alignment';
import { PDFHexString } from 'src/core';
import {
cleanText,
lineSplit,
mergeLines,
charAtIndex,
charSplit,
} from 'src/utils';
export interface TextPosition {
text: string;
encoded: PDFHexString;
x: number;
y: number;
width: number;
height: number;
}
export interface LayoutBounds {
x: number;
y: number;
width: number;
height: number;
}
const MIN_FONT_SIZE = 4;
const MAX_FONT_SIZE = 500;
const computeFontSize = (
lines: string[],
font: PDFFont,
bounds: LayoutBounds,
multiline: boolean = false,
) => {
let fontSize = MIN_FONT_SIZE;
while (fontSize < MAX_FONT_SIZE) {
let linesUsed = 0;
for (
let lineIdx = 0, lineLen = lines.length;
lineIdx < lineLen;
lineIdx++
) {
linesUsed += 1;
const line = lines[lineIdx];
const words = line.split(' ');
// Layout the words using the current `fontSize`, line wrapping
// whenever we reach the end of the current line.
let spaceInLineRemaining = bounds.width;
for (let idx = 0, len = words.length; idx < len; idx++) {
const isLastWord = idx === len - 1;
const word = isLastWord ? words[idx] : words[idx] + ' ';
const widthOfWord = font.widthOfTextAtSize(word, fontSize);
spaceInLineRemaining -= widthOfWord;
if (spaceInLineRemaining <= 0) {
linesUsed += 1;
spaceInLineRemaining = bounds.width - widthOfWord;
}
}
}
// Return if we exceeded the allowed width
if (!multiline && linesUsed > lines.length) return fontSize - 1;
const height = font.heightAtSize(fontSize);
const lineHeight = height + height * 0.2;
const totalHeight = lineHeight * linesUsed;
// Return if we exceeded the allowed height
if (totalHeight > Math.abs(bounds.height)) return fontSize - 1;
fontSize += 1;
}
return fontSize;
};
const computeCombedFontSize = (
line: string,
font: PDFFont,
bounds: LayoutBounds,
cellCount: number,
) => {
const cellWidth = bounds.width / cellCount;
const cellHeight = bounds.height;
let fontSize = MIN_FONT_SIZE;
const chars = charSplit(line);
while (fontSize < MAX_FONT_SIZE) {
for (let idx = 0, len = chars.length; idx < len; idx++) {
const c = chars[idx];
const tooLong = font.widthOfTextAtSize(c, fontSize) > cellWidth * 0.75;
if (tooLong) return fontSize - 1;
}
const height = font.heightAtSize(fontSize, { descender: false });
if (height > cellHeight) return fontSize - 1;
fontSize += 1;
}
return fontSize;
};
export interface LayoutTextOptions {
alignment: TextAlignment;
fontSize?: number;
font: PDFFont;
bounds: LayoutBounds;
}
export interface MultilineTextLayout {
bounds: LayoutBounds;
lines: TextPosition[];
fontSize: number;
lineHeight: number;
}
const lastIndexOfWhitespace = (line: string) => {
for (let idx = line.length; idx > 0; idx--) {
if (/\s/.test(line[idx])) return idx;
}
return undefined;
};
const splitOutLines = (
input: string,
maxWidth: number,
font: PDFFont,
fontSize: number,
) => {
let lastWhitespaceIdx = input.length;
while (lastWhitespaceIdx > 0) {
const line = input.substring(0, lastWhitespaceIdx);
const encoded = font.encodeText(line);
const width = font.widthOfTextAtSize(line, fontSize);
if (width < maxWidth) {
const remainder = input.substring(lastWhitespaceIdx) || undefined;
return { line, encoded, width, remainder };
}
lastWhitespaceIdx = lastIndexOfWhitespace(line) ?? 0;
}
// We were unable to split the input enough to get a chunk that would fit
// within the specified `maxWidth` so we'll just return everything
return {
line: input,
encoded: font.encodeText(input),
width: font.widthOfTextAtSize(input, fontSize),
remainder: undefined,
};
};
export const layoutMultilineText = (
text: string,
{ alignment, fontSize, font, bounds }: LayoutTextOptions,
): MultilineTextLayout => {
const lines = lineSplit(cleanText(text));
if (fontSize === undefined || fontSize === 0) {
fontSize = computeFontSize(lines, font, bounds, true);
}
const height = font.heightAtSize(fontSize);
const lineHeight = height + height * 0.2;
const textLines: TextPosition[] = [];
let minX = bounds.x;
let minY = bounds.y;
let maxX = bounds.x + bounds.width;
let maxY = bounds.y + bounds.height;
let y = bounds.y + bounds.height;
for (let idx = 0, len = lines.length; idx < len; idx++) {
let prevRemainder: string | undefined = lines[idx];
while (prevRemainder !== undefined) {
const { line, encoded, width, remainder } = splitOutLines(
prevRemainder,
bounds.width,
font,
fontSize,
);
// prettier-ignore
const x = (
alignment === TextAlignment.Left ? bounds.x
: alignment === TextAlignment.Center ? bounds.x + (bounds.width / 2) - (width / 2)
: alignment === TextAlignment.Right ? bounds.x + bounds.width - width
: bounds.x
);
y -= lineHeight;
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x + width > maxX) maxX = x + width;
if (y + height > maxY) maxY = y + height;
textLines.push({ text: line, encoded, width, height, x, y });
// Only trim lines that we had to split ourselves. So we won't trim lines
// that the user provided themselves with whitespace.
prevRemainder = remainder?.trim();
}
}
return {
fontSize,
lineHeight,
lines: textLines,
bounds: {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
},
};
};
export interface LayoutCombedTextOptions {
fontSize?: number;
font: PDFFont;
bounds: LayoutBounds;
cellCount: number;
}
export interface CombedTextLayout {
bounds: LayoutBounds;
cells: TextPosition[];
fontSize: number;
}
export const layoutCombedText = (
text: string,
{ fontSize, font, bounds, cellCount }: LayoutCombedTextOptions,
): CombedTextLayout => {
const line = mergeLines(cleanText(text));
if (line.length > cellCount) {
throw new CombedTextLayoutError(line.length, cellCount);
}
if (fontSize === undefined || fontSize === 0) {
fontSize = computeCombedFontSize(line, font, bounds, cellCount);
}
const cellWidth = bounds.width / cellCount;
const height = font.heightAtSize(fontSize, { descender: false });
const y = bounds.y + (bounds.height / 2 - height / 2);
const cells: TextPosition[] = [];
let minX = bounds.x;
let minY = bounds.y;
let maxX = bounds.x + bounds.width;
let maxY = bounds.y + bounds.height;
let cellOffset = 0;
let charOffset = 0;
while (cellOffset < cellCount) {
const [char, charLength] = charAtIndex(line, charOffset);
const encoded = font.encodeText(char);
const width = font.widthOfTextAtSize(char, fontSize);
const cellCenter = bounds.x + (cellWidth * cellOffset + cellWidth / 2);
const x = cellCenter - width / 2;
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x + width > maxX) maxX = x + width;
if (y + height > maxY) maxY = y + height;
cells.push({ text: line, encoded, width, height, x, y });
cellOffset += 1;
charOffset += charLength;
}
return {
fontSize,
cells,
bounds: {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
},
};
};
export interface LayoutSinglelineTextOptions {
alignment: TextAlignment;
fontSize?: number;
font: PDFFont;
bounds: LayoutBounds;
}
export interface SinglelineTextLayout {
bounds: LayoutBounds;
line: TextPosition;
fontSize: number;
}
export const layoutSinglelineText = (
text: string,
{ alignment, fontSize, font, bounds }: LayoutSinglelineTextOptions,
): SinglelineTextLayout => {
const line = mergeLines(cleanText(text));
if (fontSize === undefined || fontSize === 0) {
fontSize = computeFontSize([line], font, bounds);
}
const encoded = font.encodeText(line);
const width = font.widthOfTextAtSize(line, fontSize);
const height = font.heightAtSize(fontSize, { descender: false });
// prettier-ignore
const x = (
alignment === TextAlignment.Left ? bounds.x
: alignment === TextAlignment.Center ? bounds.x + (bounds.width / 2) - (width / 2)
: alignment === TextAlignment.Right ? bounds.x + bounds.width - width
: bounds.x
);
const y = bounds.y + (bounds.height / 2 - height / 2);
return {
fontSize,
line: { text: line, encoded, width, height, x, y },
bounds: { x, y, width, height },
};
};

299
node_modules/pdf-lib/src/core/PDFContext.ts generated vendored Normal file
View File

@@ -0,0 +1,299 @@
import pako from 'pako';
import PDFHeader from 'src/core/document/PDFHeader';
import { UnexpectedObjectTypeError } from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFBool from 'src/core/objects/PDFBool';
import PDFDict from 'src/core/objects/PDFDict';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFString from 'src/core/objects/PDFString';
import PDFOperator from 'src/core/operators/PDFOperator';
import Ops from 'src/core/operators/PDFOperatorNames';
import PDFContentStream from 'src/core/structures/PDFContentStream';
import { typedArrayFor } from 'src/utils';
import { SimpleRNG } from 'src/utils/rng';
type LookupKey = PDFRef | PDFObject | undefined;
interface LiteralObject {
[name: string]: Literal | PDFObject;
}
interface LiteralArray {
[index: number]: Literal | PDFObject;
}
type Literal =
| LiteralObject
| LiteralArray
| string
| number
| boolean
| null
| undefined;
const byAscendingObjectNumber = (
[a]: [PDFRef, PDFObject],
[b]: [PDFRef, PDFObject],
) => a.objectNumber - b.objectNumber;
class PDFContext {
static create = () => new PDFContext();
largestObjectNumber: number;
header: PDFHeader;
trailerInfo: {
Root?: PDFObject;
Encrypt?: PDFObject;
Info?: PDFObject;
ID?: PDFObject;
};
rng: SimpleRNG;
private readonly indirectObjects: Map<PDFRef, PDFObject>;
private pushGraphicsStateContentStreamRef?: PDFRef;
private popGraphicsStateContentStreamRef?: PDFRef;
private constructor() {
this.largestObjectNumber = 0;
this.header = PDFHeader.forVersion(1, 7);
this.trailerInfo = {};
this.indirectObjects = new Map();
this.rng = SimpleRNG.withSeed(1);
}
assign(ref: PDFRef, object: PDFObject): void {
this.indirectObjects.set(ref, object);
if (ref.objectNumber > this.largestObjectNumber) {
this.largestObjectNumber = ref.objectNumber;
}
}
nextRef(): PDFRef {
this.largestObjectNumber += 1;
return PDFRef.of(this.largestObjectNumber);
}
register(object: PDFObject): PDFRef {
const ref = this.nextRef();
this.assign(ref, object);
return ref;
}
delete(ref: PDFRef): boolean {
return this.indirectObjects.delete(ref);
}
lookupMaybe(ref: LookupKey, type: typeof PDFArray): PDFArray | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFBool): PDFBool | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFDict): PDFDict | undefined;
lookupMaybe(
ref: LookupKey,
type: typeof PDFHexString,
): PDFHexString | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFName): PDFName | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFNull): typeof PDFNull | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFNumber): PDFNumber | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFStream): PDFStream | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFRef): PDFRef | undefined;
lookupMaybe(ref: LookupKey, type: typeof PDFString): PDFString | undefined;
lookupMaybe(
ref: LookupKey,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString | undefined;
lookupMaybe(ref: LookupKey, ...types: any[]) {
// TODO: `preservePDFNull` is for backwards compatibility. Should be
// removed in next breaking API change.
const preservePDFNull = types.includes(PDFNull);
const result = ref instanceof PDFRef ? this.indirectObjects.get(ref) : ref;
if (!result || (result === PDFNull && !preservePDFNull)) return undefined;
for (let idx = 0, len = types.length; idx < len; idx++) {
const type = types[idx];
if (type === PDFNull) {
if (result === PDFNull) return result;
} else {
if (result instanceof type) return result;
}
}
throw new UnexpectedObjectTypeError(types, result);
}
lookup(ref: LookupKey): PDFObject | undefined;
lookup(ref: LookupKey, type: typeof PDFArray): PDFArray;
lookup(ref: LookupKey, type: typeof PDFBool): PDFBool;
lookup(ref: LookupKey, type: typeof PDFDict): PDFDict;
lookup(ref: LookupKey, type: typeof PDFHexString): PDFHexString;
lookup(ref: LookupKey, type: typeof PDFName): PDFName;
lookup(ref: LookupKey, type: typeof PDFNull): typeof PDFNull;
lookup(ref: LookupKey, type: typeof PDFNumber): PDFNumber;
lookup(ref: LookupKey, type: typeof PDFStream): PDFStream;
lookup(ref: LookupKey, type: typeof PDFRef): PDFRef;
lookup(ref: LookupKey, type: typeof PDFString): PDFString;
lookup(
ref: LookupKey,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString;
lookup(ref: LookupKey, ...types: any[]) {
const result = ref instanceof PDFRef ? this.indirectObjects.get(ref) : ref;
if (types.length === 0) return result;
for (let idx = 0, len = types.length; idx < len; idx++) {
const type = types[idx];
if (type === PDFNull) {
if (result === PDFNull) return result;
} else {
if (result instanceof type) return result;
}
}
throw new UnexpectedObjectTypeError(types, result);
}
getObjectRef(pdfObject: PDFObject): PDFRef | undefined {
const entries = Array.from(this.indirectObjects.entries());
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [ref, object] = entries[idx];
if (object === pdfObject) {
return ref;
}
}
return undefined;
}
enumerateIndirectObjects(): [PDFRef, PDFObject][] {
return Array.from(this.indirectObjects.entries()).sort(
byAscendingObjectNumber,
);
}
obj(literal: null | undefined): typeof PDFNull;
obj(literal: string): PDFName;
obj(literal: number): PDFNumber;
obj(literal: boolean): PDFBool;
obj(literal: LiteralObject): PDFDict;
obj(literal: LiteralArray): PDFArray;
obj(literal: Literal) {
if (literal instanceof PDFObject) {
return literal;
} else if (literal === null || literal === undefined) {
return PDFNull;
} else if (typeof literal === 'string') {
return PDFName.of(literal);
} else if (typeof literal === 'number') {
return PDFNumber.of(literal);
} else if (typeof literal === 'boolean') {
return literal ? PDFBool.True : PDFBool.False;
} else if (Array.isArray(literal)) {
const array = PDFArray.withContext(this);
for (let idx = 0, len = literal.length; idx < len; idx++) {
array.push(this.obj(literal[idx]));
}
return array;
} else {
const dict = PDFDict.withContext(this);
const keys = Object.keys(literal);
for (let idx = 0, len = keys.length; idx < len; idx++) {
const key = keys[idx];
const value = (literal as LiteralObject)[key] as any;
if (value !== undefined) dict.set(PDFName.of(key), this.obj(value));
}
return dict;
}
}
stream(
contents: string | Uint8Array,
dict: LiteralObject = {},
): PDFRawStream {
return PDFRawStream.of(this.obj(dict), typedArrayFor(contents));
}
flateStream(
contents: string | Uint8Array,
dict: LiteralObject = {},
): PDFRawStream {
return this.stream(pako.deflate(typedArrayFor(contents)), {
...dict,
Filter: 'FlateDecode',
});
}
contentStream(
operators: PDFOperator[],
dict: LiteralObject = {},
): PDFContentStream {
return PDFContentStream.of(this.obj(dict), operators);
}
formXObject(
operators: PDFOperator[],
dict: LiteralObject = {},
): PDFContentStream {
return this.contentStream(operators, {
BBox: this.obj([0, 0, 0, 0]),
Matrix: this.obj([1, 0, 0, 1, 0, 0]),
...dict,
Type: 'XObject',
Subtype: 'Form',
});
}
/*
* Reference to PDFContentStream that contains a single PDFOperator: `q`.
* Used by [[PDFPageLeaf]] instances to ensure that when content streams are
* added to a modified PDF, they start in the default, unchanged graphics
* state.
*/
getPushGraphicsStateContentStream(): PDFRef {
if (this.pushGraphicsStateContentStreamRef) {
return this.pushGraphicsStateContentStreamRef;
}
const dict = this.obj({});
const op = PDFOperator.of(Ops.PushGraphicsState);
const stream = PDFContentStream.of(dict, [op]);
this.pushGraphicsStateContentStreamRef = this.register(stream);
return this.pushGraphicsStateContentStreamRef;
}
/*
* Reference to PDFContentStream that contains a single PDFOperator: `Q`.
* Used by [[PDFPageLeaf]] instances to ensure that when content streams are
* added to a modified PDF, they start in the default, unchanged graphics
* state.
*/
getPopGraphicsStateContentStream(): PDFRef {
if (this.popGraphicsStateContentStreamRef) {
return this.popGraphicsStateContentStreamRef;
}
const dict = this.obj({});
const op = PDFOperator.of(Ops.PopGraphicsState);
const stream = PDFContentStream.of(dict, [op]);
this.popGraphicsStateContentStreamRef = this.register(stream);
return this.popGraphicsStateContentStreamRef;
}
addRandomSuffix(prefix: string, suffixLength = 4): string {
return `${prefix}-${Math.floor(this.rng.nextInt() * 10 ** suffixLength)}`;
}
}
export default PDFContext;

143
node_modules/pdf-lib/src/core/PDFObjectCopier.ts generated vendored Normal file
View File

@@ -0,0 +1,143 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFContext from 'src/core/PDFContext';
import PDFPageLeaf from 'src/core/structures/PDFPageLeaf';
/**
* PDFObjectCopier copies PDFObjects from a src context to a dest context.
* The primary use case for this is to copy pages between PDFs.
*
* _Copying_ an object with a PDFObjectCopier is different from _cloning_ an
* object with its [[PDFObject.clone]] method:
*
* ```
* const src: PDFContext = ...
* const dest: PDFContext = ...
* const originalObject: PDFObject = ...
* const copiedObject = PDFObjectCopier.for(src, dest).copy(originalObject);
* const clonedObject = originalObject.clone();
* ```
*
* Copying an object is equivalent to cloning it and then copying over any other
* objects that it references. Note that only dictionaries, arrays, and streams
* (or structures build from them) can contain indirect references to other
* objects. Copying a PDFObject that is not a dictionary, array, or stream is
* supported, but is equivalent to cloning it.
*/
class PDFObjectCopier {
static for = (src: PDFContext, dest: PDFContext) =>
new PDFObjectCopier(src, dest);
private readonly src: PDFContext;
private readonly dest: PDFContext;
private readonly traversedObjects = new Map<PDFObject, PDFObject>();
private constructor(src: PDFContext, dest: PDFContext) {
this.src = src;
this.dest = dest;
}
// prettier-ignore
copy = <T extends PDFObject>(object: T): T => (
object instanceof PDFPageLeaf ? this.copyPDFPage(object)
: object instanceof PDFDict ? this.copyPDFDict(object)
: object instanceof PDFArray ? this.copyPDFArray(object)
: object instanceof PDFStream ? this.copyPDFStream(object)
: object instanceof PDFRef ? this.copyPDFIndirectObject(object)
: object.clone()
) as T;
private copyPDFPage = (originalPage: PDFPageLeaf): PDFPageLeaf => {
const clonedPage = originalPage.clone();
// Move any entries that the originalPage is inheriting from its parent
// tree nodes directly into originalPage so they are preserved during
// the copy.
const { InheritableEntries } = PDFPageLeaf;
for (let idx = 0, len = InheritableEntries.length; idx < len; idx++) {
const key = PDFName.of(InheritableEntries[idx]);
const value = clonedPage.getInheritableAttribute(key)!;
if (!clonedPage.get(key) && value) clonedPage.set(key, value);
}
// Remove the parent reference to prevent the whole donor document's page
// tree from being copied when we only need a single page.
clonedPage.delete(PDFName.of('Parent'));
return this.copyPDFDict(clonedPage) as PDFPageLeaf;
};
private copyPDFDict = (originalDict: PDFDict): PDFDict => {
if (this.traversedObjects.has(originalDict)) {
return this.traversedObjects.get(originalDict) as PDFDict;
}
const clonedDict = originalDict.clone(this.dest);
this.traversedObjects.set(originalDict, clonedDict);
const entries = originalDict.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
clonedDict.set(key, this.copy(value));
}
return clonedDict;
};
private copyPDFArray = (originalArray: PDFArray): PDFArray => {
if (this.traversedObjects.has(originalArray)) {
return this.traversedObjects.get(originalArray) as PDFArray;
}
const clonedArray = originalArray.clone(this.dest);
this.traversedObjects.set(originalArray, clonedArray);
for (let idx = 0, len = originalArray.size(); idx < len; idx++) {
const value = originalArray.get(idx);
clonedArray.set(idx, this.copy(value));
}
return clonedArray;
};
private copyPDFStream = (originalStream: PDFStream): PDFStream => {
if (this.traversedObjects.has(originalStream)) {
return this.traversedObjects.get(originalStream) as PDFStream;
}
const clonedStream = originalStream.clone(this.dest);
this.traversedObjects.set(originalStream, clonedStream);
const entries = originalStream.dict.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
clonedStream.dict.set(key, this.copy(value));
}
return clonedStream;
};
private copyPDFIndirectObject = (ref: PDFRef): PDFRef => {
const alreadyMapped = this.traversedObjects.has(ref);
if (!alreadyMapped) {
const newRef = this.dest.nextRef();
this.traversedObjects.set(ref, newRef);
const dereferencedValue = this.src.lookup(ref);
if (dereferencedValue) {
const cloned = this.copy(dereferencedValue);
this.dest.assign(newRef, cloned);
}
}
return this.traversedObjects.get(ref) as PDFRef;
};
}
export default PDFObjectCopier;

114
node_modules/pdf-lib/src/core/acroform/PDFAcroButton.ts generated vendored Normal file
View File

@@ -0,0 +1,114 @@
import PDFObject from 'src/core/objects/PDFObject';
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFArray from 'src/core/objects/PDFArray';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
import { IndexOutOfBoundsError } from 'src/core/errors';
class PDFAcroButton extends PDFAcroTerminal {
Opt(): PDFString | PDFHexString | PDFArray | undefined {
return this.dict.lookupMaybe(
PDFName.of('Opt'),
PDFString,
PDFHexString,
PDFArray,
);
}
setOpt(opt: PDFObject[]) {
this.dict.set(PDFName.of('Opt'), this.dict.context.obj(opt));
}
getExportValues(): (PDFString | PDFHexString)[] | undefined {
const opt = this.Opt();
if (!opt) return undefined;
if (opt instanceof PDFString || opt instanceof PDFHexString) {
return [opt];
}
const values: (PDFString | PDFHexString)[] = [];
for (let idx = 0, len = opt.size(); idx < len; idx++) {
const value = opt.lookup(idx);
if (value instanceof PDFString || value instanceof PDFHexString) {
values.push(value);
}
}
return values;
}
removeExportValue(idx: number) {
const opt = this.Opt();
if (!opt) return;
if (opt instanceof PDFString || opt instanceof PDFHexString) {
if (idx !== 0) throw new IndexOutOfBoundsError(idx, 0, 0);
this.setOpt([]);
} else {
if (idx < 0 || idx > opt.size()) {
throw new IndexOutOfBoundsError(idx, 0, opt.size());
}
opt.remove(idx);
}
}
// Enforce use use of /Opt even if it isn't strictly necessary
normalizeExportValues() {
const exportValues = this.getExportValues() ?? [];
const Opt: (PDFString | PDFHexString)[] = [];
const widgets = this.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const exportVal =
exportValues[idx] ??
PDFHexString.fromText(widget.getOnValue()?.decodeText() ?? '');
Opt.push(exportVal);
}
this.setOpt(Opt);
}
/**
* Reuses existing opt if one exists with the same value (assuming
* `useExistingIdx` is `true`). Returns index of existing (or new) opt.
*/
addOpt(opt: PDFHexString | PDFString, useExistingOptIdx: boolean): number {
this.normalizeExportValues();
const optText = opt.decodeText();
let existingIdx: number | undefined;
if (useExistingOptIdx) {
const exportValues = this.getExportValues() ?? [];
for (let idx = 0, len = exportValues.length; idx < len; idx++) {
const exportVal = exportValues[idx];
if (exportVal.decodeText() === optText) existingIdx = idx;
}
}
const Opt = this.Opt() as PDFArray;
Opt.push(opt);
return existingIdx ?? Opt.size() - 1;
}
addWidgetWithOpt(
widget: PDFRef,
opt: PDFHexString | PDFString,
useExistingOptIdx: boolean,
) {
const optIdx = this.addOpt(opt, useExistingOptIdx);
const apStateValue = PDFName.of(String(optIdx));
this.addWidget(widget);
return apStateValue;
}
}
export default PDFAcroButton;

View File

@@ -0,0 +1,49 @@
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFAcroButton from 'src/core/acroform/PDFAcroButton';
import { InvalidAcroFieldValueError } from 'src/core/errors';
class PDFAcroCheckBox extends PDFAcroButton {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroCheckBox(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Btn',
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroCheckBox(dict, ref);
};
setValue(value: PDFName) {
const onValue = this.getOnValue() ?? PDFName.of('Yes');
if (value !== onValue && value !== PDFName.of('Off')) {
throw new InvalidAcroFieldValueError();
}
this.dict.set(PDFName.of('V'), value);
const widgets = this.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const state = widget.getOnValue() === value ? value : PDFName.of('Off');
widget.setAppearanceState(state);
}
}
getValue(): PDFName {
const v = this.V();
if (v instanceof PDFName) return v;
return PDFName.of('Off');
}
getOnValue(): PDFName | undefined {
const [widget] = this.getWidgets();
return widget?.getOnValue();
}
}
export default PDFAcroCheckBox;

153
node_modules/pdf-lib/src/core/acroform/PDFAcroChoice.ts generated vendored Normal file
View File

@@ -0,0 +1,153 @@
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFString from 'src/core/objects/PDFString';
import PDFArray from 'src/core/objects/PDFArray';
import PDFName from 'src/core/objects/PDFName';
import { AcroChoiceFlags } from 'src/core/acroform/flags';
import {
InvalidAcroFieldValueError,
MultiSelectValueError,
} from 'src/core/errors';
class PDFAcroChoice extends PDFAcroTerminal {
setValues(values: (PDFString | PDFHexString)[]) {
if (
this.hasFlag(AcroChoiceFlags.Combo) &&
!this.hasFlag(AcroChoiceFlags.Edit) &&
!this.valuesAreValid(values)
) {
throw new InvalidAcroFieldValueError();
}
if (values.length === 0) {
this.dict.delete(PDFName.of('V'));
}
if (values.length === 1) {
this.dict.set(PDFName.of('V'), values[0]);
}
if (values.length > 1) {
if (!this.hasFlag(AcroChoiceFlags.MultiSelect)) {
throw new MultiSelectValueError();
}
this.dict.set(PDFName.of('V'), this.dict.context.obj(values));
}
this.updateSelectedIndices(values);
}
valuesAreValid(values: (PDFString | PDFHexString)[]): boolean {
const options = this.getOptions();
for (let idx = 0, len = values.length; idx < len; idx++) {
const val = values[idx].decodeText();
if (!options.find((o) => val === (o.display || o.value).decodeText())) {
return false;
}
}
return true;
}
updateSelectedIndices(values: (PDFString | PDFHexString)[]) {
if (values.length > 1) {
const indices = new Array<number>(values.length);
const options = this.getOptions();
for (let idx = 0, len = values.length; idx < len; idx++) {
const val = values[idx].decodeText();
indices[idx] = options.findIndex(
(o) => val === (o.display || o.value).decodeText(),
);
}
this.dict.set(PDFName.of('I'), this.dict.context.obj(indices.sort()));
} else {
this.dict.delete(PDFName.of('I'));
}
}
getValues(): (PDFString | PDFHexString)[] {
const v = this.V();
if (v instanceof PDFString || v instanceof PDFHexString) return [v];
if (v instanceof PDFArray) {
const values: (PDFString | PDFHexString)[] = [];
for (let idx = 0, len = v.size(); idx < len; idx++) {
const value = v.lookup(idx);
if (value instanceof PDFString || value instanceof PDFHexString) {
values.push(value);
}
}
return values;
}
return [];
}
Opt(): PDFArray | PDFString | PDFHexString | undefined {
return this.dict.lookupMaybe(
PDFName.of('Opt'),
PDFString,
PDFHexString,
PDFArray,
);
}
setOptions(
options: {
value: PDFString | PDFHexString;
display?: PDFString | PDFHexString;
}[],
) {
const newOpt = new Array<PDFArray>(options.length);
for (let idx = 0, len = options.length; idx < len; idx++) {
const { value, display } = options[idx];
newOpt[idx] = this.dict.context.obj([value, display || value]);
}
this.dict.set(PDFName.of('Opt'), this.dict.context.obj(newOpt));
}
getOptions(): {
value: PDFString | PDFHexString;
display: PDFString | PDFHexString;
}[] {
const Opt = this.Opt();
// Not supposed to happen - Opt _should_ always be `PDFArray | undefined`
if (Opt instanceof PDFString || Opt instanceof PDFHexString) {
return [{ value: Opt, display: Opt }];
}
if (Opt instanceof PDFArray) {
const res: {
value: PDFString | PDFHexString;
display: PDFString | PDFHexString;
}[] = [];
for (let idx = 0, len = Opt.size(); idx < len; idx++) {
const item = Opt.lookup(idx);
// If `item` is a string, use that as both the export and text value
if (item instanceof PDFString || item instanceof PDFHexString) {
res.push({ value: item, display: item });
}
// If `item` is an array of one, treat it the same as just a string,
// if it's an array of two then `item[0]` is the export value and
// `item[1]` is the text value
if (item instanceof PDFArray) {
if (item.size() > 0) {
const first = item.lookup(0, PDFString, PDFHexString);
const second = item.lookupMaybe(1, PDFString, PDFHexString);
res.push({ value: first, display: second || first });
}
}
}
return res;
}
return [];
}
}
export default PDFAcroChoice;

View File

@@ -0,0 +1,22 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFAcroChoice from 'src/core/acroform/PDFAcroChoice';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
import { AcroChoiceFlags } from 'src/core/acroform/flags';
class PDFAcroComboBox extends PDFAcroChoice {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroComboBox(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Ch',
Ff: AcroChoiceFlags.Combo,
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroComboBox(dict, ref);
};
}
export default PDFAcroComboBox;

167
node_modules/pdf-lib/src/core/acroform/PDFAcroField.ts generated vendored Normal file
View File

@@ -0,0 +1,167 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFObject from 'src/core/objects/PDFObject';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFArray from 'src/core/objects/PDFArray';
import PDFRef from 'src/core/objects/PDFRef';
import { findLastMatch } from 'src/utils';
import { MissingDAEntryError, MissingTfOperatorError } from 'src/core/errors';
// Examples:
// `/Helv 12 Tf` -> ['Helv', '12']
// `/HeBo 8.00 Tf` -> ['HeBo', '8.00']
// `/HeBo Tf` -> ['HeBo', undefined]
const tfRegex = /\/([^\0\t\n\f\r\ ]+)[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]+Tf/;
class PDFAcroField {
readonly dict: PDFDict;
readonly ref: PDFRef;
protected constructor(dict: PDFDict, ref: PDFRef) {
this.dict = dict;
this.ref = ref;
}
T(): PDFString | PDFHexString | undefined {
return this.dict.lookupMaybe(PDFName.of('T'), PDFString, PDFHexString);
}
Ff(): PDFNumber | undefined {
const numberOrRef = this.getInheritableAttribute(PDFName.of('Ff'));
return this.dict.context.lookupMaybe(numberOrRef, PDFNumber);
}
V(): PDFObject | undefined {
const valueOrRef = this.getInheritableAttribute(PDFName.of('V'));
return this.dict.context.lookup(valueOrRef);
}
Kids(): PDFArray | undefined {
return this.dict.lookupMaybe(PDFName.of('Kids'), PDFArray);
}
// Parent(): PDFDict | undefined {
// return this.dict.lookupMaybe(PDFName.of('Parent'), PDFDict);
// }
DA(): PDFString | PDFHexString | undefined {
const da = this.dict.lookup(PDFName.of('DA'));
if (da instanceof PDFString || da instanceof PDFHexString) return da;
return undefined;
}
setKids(kids: PDFObject[]) {
this.dict.set(PDFName.of('Kids'), this.dict.context.obj(kids));
}
getParent(): PDFAcroField | undefined {
// const parent = this.Parent();
// if (!parent) return undefined;
// return new PDFAcroField(parent);
const parentRef = this.dict.get(PDFName.of('Parent'));
if (parentRef instanceof PDFRef) {
const parent = this.dict.lookup(PDFName.of('Parent'), PDFDict);
return new PDFAcroField(parent, parentRef);
}
return undefined;
}
setParent(parent: PDFRef | undefined) {
if (!parent) this.dict.delete(PDFName.of('Parent'));
else this.dict.set(PDFName.of('Parent'), parent);
}
getFullyQualifiedName(): string | undefined {
const parent = this.getParent();
if (!parent) return this.getPartialName();
return `${parent.getFullyQualifiedName()}.${this.getPartialName()}`;
}
getPartialName(): string | undefined {
return this.T()?.decodeText();
}
setPartialName(partialName: string | undefined) {
if (!partialName) this.dict.delete(PDFName.of('T'));
else this.dict.set(PDFName.of('T'), PDFHexString.fromText(partialName));
}
setDefaultAppearance(appearance: string) {
this.dict.set(PDFName.of('DA'), PDFString.of(appearance));
}
getDefaultAppearance(): string | undefined {
const DA = this.DA();
if (DA instanceof PDFHexString) {
return DA.decodeText();
}
return DA?.asString();
}
setFontSize(fontSize: number) {
const name = this.getFullyQualifiedName() ?? '';
const da = this.getDefaultAppearance();
if (!da) throw new MissingDAEntryError(name);
const daMatch = findLastMatch(da, tfRegex);
if (!daMatch.match) throw new MissingTfOperatorError(name);
const daStart = da.slice(0, daMatch.pos - daMatch.match[0].length);
const daEnd = daMatch.pos <= da.length ? da.slice(daMatch.pos) : '';
const fontName = daMatch.match[1];
const modifiedDa = `${daStart} /${fontName} ${fontSize} Tf ${daEnd}`;
this.setDefaultAppearance(modifiedDa);
}
getFlags(): number {
return this.Ff()?.asNumber() ?? 0;
}
setFlags(flags: number) {
this.dict.set(PDFName.of('Ff'), PDFNumber.of(flags));
}
hasFlag(flag: number): boolean {
const flags = this.getFlags();
return (flags & flag) !== 0;
}
setFlag(flag: number) {
const flags = this.getFlags();
this.setFlags(flags | flag);
}
clearFlag(flag: number) {
const flags = this.getFlags();
this.setFlags(flags & ~flag);
}
setFlagTo(flag: number, enable: boolean) {
if (enable) this.setFlag(flag);
else this.clearFlag(flag);
}
getInheritableAttribute(name: PDFName): PDFObject | undefined {
let attribute: PDFObject | undefined;
this.ascend((node) => {
if (!attribute) attribute = node.dict.get(name);
});
return attribute;
}
ascend(visitor: (node: PDFAcroField) => any): void {
visitor(this);
const parent = this.getParent();
if (parent) parent.ascend(visitor);
}
}
export default PDFAcroField;

102
node_modules/pdf-lib/src/core/acroform/PDFAcroForm.ts generated vendored Normal file
View File

@@ -0,0 +1,102 @@
import PDFContext from 'src/core/PDFContext';
import PDFDict from 'src/core/objects/PDFDict';
import PDFArray from 'src/core/objects/PDFArray';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroField from 'src/core/acroform/PDFAcroField';
import PDFAcroNonTerminal from 'src/core/acroform/PDFAcroNonTerminal';
import {
createPDFAcroField,
createPDFAcroFields,
} from 'src/core/acroform/utils';
class PDFAcroForm {
readonly dict: PDFDict;
static fromDict = (dict: PDFDict) => new PDFAcroForm(dict);
static create = (context: PDFContext) => {
const dict = context.obj({ Fields: [] });
return new PDFAcroForm(dict);
};
private constructor(dict: PDFDict) {
this.dict = dict;
}
Fields(): PDFArray | undefined {
const fields = this.dict.lookup(PDFName.of('Fields'));
if (fields instanceof PDFArray) return fields;
return undefined;
}
getFields(): [PDFAcroField, PDFRef][] {
const { Fields } = this.normalizedEntries();
const fields = new Array(Fields.size());
for (let idx = 0, len = Fields.size(); idx < len; idx++) {
const ref = Fields.get(idx) as PDFRef;
const dict = Fields.lookup(idx, PDFDict);
fields[idx] = [createPDFAcroField(dict, ref), ref];
}
return fields;
}
getAllFields(): [PDFAcroField, PDFRef][] {
const allFields: [PDFAcroField, PDFRef][] = [];
const pushFields = (fields?: [PDFAcroField, PDFRef][]) => {
if (!fields) return;
for (let idx = 0, len = fields.length; idx < len; idx++) {
const field = fields[idx];
allFields.push(field);
const [fieldModel] = field;
if (fieldModel instanceof PDFAcroNonTerminal) {
pushFields(createPDFAcroFields(fieldModel.Kids()));
}
}
};
pushFields(this.getFields());
return allFields;
}
addField(field: PDFRef) {
const { Fields } = this.normalizedEntries();
Fields?.push(field);
}
removeField(field: PDFAcroField): void {
const parent = field.getParent();
const fields =
parent === undefined ? this.normalizedEntries().Fields : parent.Kids();
const index = fields?.indexOf(field.ref);
if (fields === undefined || index === undefined) {
throw new Error(
`Tried to remove inexistent field ${field.getFullyQualifiedName()}`,
);
}
fields.remove(index);
if (parent !== undefined && fields.size() === 0) {
this.removeField(parent);
}
}
normalizedEntries() {
let Fields = this.Fields();
if (!Fields) {
Fields = this.dict.context.obj([]);
this.dict.set(PDFName.of('Fields'), Fields);
}
return { Fields };
}
}
export default PDFAcroForm;

View File

@@ -0,0 +1,20 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFAcroChoice from 'src/core/acroform/PDFAcroChoice';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
class PDFAcroListBox extends PDFAcroChoice {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroListBox(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Ch',
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroListBox(dict, ref);
};
}
export default PDFAcroListBox;

View File

@@ -0,0 +1,34 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFRef from 'src/core/objects/PDFRef';
import PDFName from 'src/core/objects/PDFName';
import PDFContext from 'src/core/PDFContext';
import PDFAcroField from 'src/core/acroform/PDFAcroField';
class PDFAcroNonTerminal extends PDFAcroField {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroNonTerminal(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({});
const ref = context.register(dict);
return new PDFAcroNonTerminal(dict, ref);
};
addField(field: PDFRef) {
const { Kids } = this.normalizedEntries();
Kids?.push(field);
}
normalizedEntries() {
let Kids = this.Kids();
if (!Kids) {
Kids = this.dict.context.obj([]);
this.dict.set(PDFName.of('Kids'), Kids);
}
return { Kids };
}
}
export default PDFAcroNonTerminal;

View File

@@ -0,0 +1,22 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFAcroButton from 'src/core/acroform/PDFAcroButton';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
import { AcroButtonFlags } from 'src/core/acroform/flags';
class PDFAcroPushButton extends PDFAcroButton {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroPushButton(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Btn',
Ff: AcroButtonFlags.PushButton,
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroPushButton(dict, ref);
};
}
export default PDFAcroPushButton;

View File

@@ -0,0 +1,58 @@
import PDFRef from 'src/core/objects/PDFRef';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFAcroButton from 'src/core/acroform/PDFAcroButton';
import PDFContext from 'src/core/PDFContext';
import { AcroButtonFlags } from 'src/core/acroform/flags';
import { InvalidAcroFieldValueError } from 'src/core/errors';
class PDFAcroRadioButton extends PDFAcroButton {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroRadioButton(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Btn',
Ff: AcroButtonFlags.Radio,
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroRadioButton(dict, ref);
};
setValue(value: PDFName) {
const onValues = this.getOnValues();
if (!onValues.includes(value) && value !== PDFName.of('Off')) {
throw new InvalidAcroFieldValueError();
}
this.dict.set(PDFName.of('V'), value);
const widgets = this.getWidgets();
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const widget = widgets[idx];
const state = widget.getOnValue() === value ? value : PDFName.of('Off');
widget.setAppearanceState(state);
}
}
getValue(): PDFName {
const v = this.V();
if (v instanceof PDFName) return v;
return PDFName.of('Off');
}
getOnValues(): PDFName[] {
const widgets = this.getWidgets();
const onValues: PDFName[] = [];
for (let idx = 0, len = widgets.length; idx < len; idx++) {
const onValue = widgets[idx].getOnValue();
if (onValue) onValues.push(onValue);
}
return onValues;
}
}
export default PDFAcroRadioButton;

View File

@@ -0,0 +1,10 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
class PDFAcroSignature extends PDFAcroTerminal {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroSignature(dict, ref);
}
export default PDFAcroSignature;

View File

@@ -0,0 +1,71 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroField from 'src/core/acroform/PDFAcroField';
import PDFWidgetAnnotation from 'src/core/annotation/PDFWidgetAnnotation';
import { IndexOutOfBoundsError } from 'src/core/errors';
class PDFAcroTerminal extends PDFAcroField {
static fromDict = (dict: PDFDict, ref: PDFRef) =>
new PDFAcroTerminal(dict, ref);
FT(): PDFName {
const nameOrRef = this.getInheritableAttribute(PDFName.of('FT'));
return this.dict.context.lookup(nameOrRef, PDFName);
}
getWidgets(): PDFWidgetAnnotation[] {
const kidDicts = this.Kids();
// This field is itself a widget
if (!kidDicts) return [PDFWidgetAnnotation.fromDict(this.dict)];
// This field's kids are its widgets
const widgets = new Array<PDFWidgetAnnotation>(kidDicts.size());
for (let idx = 0, len = kidDicts.size(); idx < len; idx++) {
const dict = kidDicts.lookup(idx, PDFDict);
widgets[idx] = PDFWidgetAnnotation.fromDict(dict);
}
return widgets;
}
addWidget(ref: PDFRef) {
const { Kids } = this.normalizedEntries();
Kids.push(ref);
}
removeWidget(idx: number) {
const kidDicts = this.Kids();
if (!kidDicts) {
// This field is itself a widget
if (idx !== 0) throw new IndexOutOfBoundsError(idx, 0, 0);
this.setKids([]);
} else {
// This field's kids are its widgets
if (idx < 0 || idx > kidDicts.size()) {
throw new IndexOutOfBoundsError(idx, 0, kidDicts.size());
}
kidDicts.remove(idx);
}
}
normalizedEntries() {
let Kids = this.Kids();
// If this field is itself a widget (because it was only rendered once in
// the document, so the field and widget properties were merged) then we
// add itself to the `Kids` array. The alternative would be to try
// splitting apart the widget properties and creating a separate object
// for them.
if (!Kids) {
Kids = this.dict.context.obj([this.ref]);
this.dict.set(PDFName.of('Kids'), Kids);
}
return { Kids };
}
}
export default PDFAcroTerminal;

76
node_modules/pdf-lib/src/core/acroform/PDFAcroText.ts generated vendored Normal file
View File

@@ -0,0 +1,76 @@
import PDFContext from 'src/core/PDFContext';
import PDFDict from 'src/core/objects/PDFDict';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
class PDFAcroText extends PDFAcroTerminal {
static fromDict = (dict: PDFDict, ref: PDFRef) => new PDFAcroText(dict, ref);
static create = (context: PDFContext) => {
const dict = context.obj({
FT: 'Tx',
Kids: [],
});
const ref = context.register(dict);
return new PDFAcroText(dict, ref);
};
MaxLen(): PDFNumber | undefined {
const maxLen = this.dict.lookup(PDFName.of('MaxLen'));
if (maxLen instanceof PDFNumber) return maxLen;
return undefined;
}
Q(): PDFNumber | undefined {
const q = this.dict.lookup(PDFName.of('Q'));
if (q instanceof PDFNumber) return q;
return undefined;
}
setMaxLength(maxLength: number) {
this.dict.set(PDFName.of('MaxLen'), PDFNumber.of(maxLength));
}
removeMaxLength() {
this.dict.delete(PDFName.of('MaxLen'));
}
getMaxLength(): number | undefined {
return this.MaxLen()?.asNumber();
}
setQuadding(quadding: 0 | 1 | 2) {
this.dict.set(PDFName.of('Q'), PDFNumber.of(quadding));
}
getQuadding(): number | undefined {
return this.Q()?.asNumber();
}
setValue(value: PDFHexString | PDFString) {
this.dict.set(PDFName.of('V'), value);
// const widgets = this.getWidgets();
// for (let idx = 0, len = widgets.length; idx < len; idx++) {
// const widget = widgets[idx];
// const state = widget.getOnValue() === value ? value : PDFName.of('Off');
// widget.setAppearanceState(state);
// }
}
removeValue() {
this.dict.delete(PDFName.of('V'));
}
getValue(): PDFString | PDFHexString | undefined {
const v = this.V();
if (v instanceof PDFString || v instanceof PDFHexString) return v;
return undefined;
}
}
export default PDFAcroText;

162
node_modules/pdf-lib/src/core/acroform/flags.ts generated vendored Normal file
View File

@@ -0,0 +1,162 @@
const flag = (bitIndex: number) => 1 << bitIndex;
/** From PDF spec table 221 */
export enum AcroFieldFlags {
/**
* If set, the user may not change the value of the field. Any associated
* widget annotations will not interact with the user; that is, they will not
* respond to mouse clicks or change their appearance in response to mouse
* motions. This flag is useful for fields whose values are computed or
* imported from a database.
*/
ReadOnly = flag(1 - 1),
/**
* If set, the field shall have a value at the time it is exported by a
* submit-form action (see 12.7.5.2, "Submit-Form Action").
*/
Required = flag(2 - 1),
/**
* If set, the field shall not be exported by a submit-form action
* (see 12.7.5.2, "Submit-Form Action").
*/
NoExport = flag(3 - 1),
}
/** From PDF spec table 226 */
export enum AcroButtonFlags {
/**
* (Radio buttons only) If set, exactly one radio button shall be selected at
* all times; selecting the currently selected button has no effect. If clear,
* clicking the selected button deselects it, leaving no button selected.
*/
NoToggleToOff = flag(15 - 1),
/**
* If set, the field is a set of radio buttons; if clear, the field is a check
* box. This flag may be set only if the Pushbutton flag is clear.
*/
Radio = flag(16 - 1),
/**
* If set, the field is a pushbutton that does not retain a permanent value.
*/
PushButton = flag(17 - 1),
/**
* If set, a group of radio buttons within a radio button field that use the
* same value for the on state will turn on and off in unison; that is if one
* is checked, they are all checked. If clear, the buttons are mutually
* exclusive (the same behavior as HTML radio buttons).
*/
RadiosInUnison = flag(26 - 1),
}
/** From PDF spec table 228 */
export enum AcroTextFlags {
/**
* If set, the field may contain multiple lines of text; if clear, the field's
* text shall be restricted to a single line.
*/
Multiline = flag(13 - 1),
/**
* If set, the field is intended for entering a secure password that should
* not be echoed visibly to the screen. Characters typed from the keyboard
* shall instead be echoed in some unreadable form, such as asterisks or
* bullet characters.
* > NOTE To protect password confidentiality, readers should never store
* > the value of the text field in the PDF file if this flag is set.
*/
Password = flag(14 - 1),
/**
* If set, the text entered in the field represents the pathname of a file
* whose contents shall be submitted as the value of the field.
*/
FileSelect = flag(21 - 1),
/**
* If set, text entered in the field shall not be spell-checked.
*/
DoNotSpellCheck = flag(23 - 1),
/**
* If set, the field shall not scroll (horizontally for single-line fields,
* vertically for multiple-line fields) to accommodate more text than fits
* within its annotation rectangle. Once the field is full, no further text
* shall be accepted for interactive form filling; for non-interactive form
* filling, the filler should take care not to add more character than will
* visibly fit in the defined area.
*/
DoNotScroll = flag(24 - 1),
/**
* May be set only if the MaxLen entry is present in the text field dictionary
* (see Table 229) and if the Multiline, Password, and FileSelect flags are
* clear. If set, the field shall be automatically divided into as many
* equally spaced positions, or combs, as the value of MaxLen, and the text
* is laid out into those combs.
*/
Comb = flag(25 - 1),
/**
* If set, the value of this field shall be a rich text string
* (see 12.7.3.4, "Rich Text Strings"). If the field has a value, the RV
* entry of the field dictionary (Table 222) shall specify the rich text
* string.
*/
RichText = flag(26 - 1),
}
/** From PDF spec table 230 */
export enum AcroChoiceFlags {
/**
* If set, the field is a combo box; if clear, the field is a list box.
*/
Combo = flag(18 - 1),
/**
* If set, the combo box shall include an editable text box as well as a
* drop-down list; if clear, it shall include only a drop-down list. This
* flag shall be used only if the Combo flag is set.
*/
Edit = flag(19 - 1),
/**
* If set, the field's option items shall be sorted alphabetically. This flag
* is intended for use by writers, not by readers. Conforming readers shall
* display the options in the order in which they occur in the Opt array
* (see Table 231).
*/
Sort = flag(20 - 1),
/**
* If set, more than one of the field's option items may be selected
* simultaneously; if clear, at most one item shall be selected.
*/
MultiSelect = flag(22 - 1),
/**
* If set, text entered in the field shall not be spell-checked. This flag
* shall not be used unless the Combo and Edit flags are both set.
*/
DoNotSpellCheck = flag(23 - 1),
/**
* If set, the new value shall be committed as soon as a selection is made
* (commonly with the pointing device). In this case, supplying a value for
* a field involves three actions: selecting the field for fill-in,
* selecting a choice for the fill-in value, and leaving that field, which
* finalizes or "commits" the data choice and triggers any actions associated
* with the entry or changing of this data. If this flag is on, then
* processing does not wait for leaving the field action to occur, but
* immediately proceeds to the third step.
*
* This option enables applications to perform an action once a selection is
* made, without requiring the user to exit the field. If clear, the new
* value is not committed until the user exits the field.
*/
CommitOnSelChange = flag(27 - 1),
}

15
node_modules/pdf-lib/src/core/acroform/index.ts generated vendored Normal file
View File

@@ -0,0 +1,15 @@
export { default as PDFAcroButton } from 'src/core/acroform/PDFAcroButton';
export { default as PDFAcroCheckBox } from 'src/core/acroform/PDFAcroCheckBox';
export { default as PDFAcroChoice } from 'src/core/acroform/PDFAcroChoice';
export { default as PDFAcroComboBox } from 'src/core/acroform/PDFAcroComboBox';
export { default as PDFAcroField } from 'src/core/acroform/PDFAcroField';
export { default as PDFAcroForm } from 'src/core/acroform/PDFAcroForm';
export { default as PDFAcroListBox } from 'src/core/acroform/PDFAcroListBox';
export { default as PDFAcroNonTerminal } from 'src/core/acroform/PDFAcroNonTerminal';
export { default as PDFAcroPushButton } from 'src/core/acroform/PDFAcroPushButton';
export { default as PDFAcroRadioButton } from 'src/core/acroform/PDFAcroRadioButton';
export { default as PDFAcroSignature } from 'src/core/acroform/PDFAcroSignature';
export { default as PDFAcroTerminal } from 'src/core/acroform/PDFAcroTerminal';
export { default as PDFAcroText } from 'src/core/acroform/PDFAcroText';
export * from 'src/core/acroform/flags';
export * from 'src/core/acroform/utils';

135
node_modules/pdf-lib/src/core/acroform/utils.ts generated vendored Normal file
View File

@@ -0,0 +1,135 @@
import PDFObject from 'src/core/objects/PDFObject';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFArray from 'src/core/objects/PDFArray';
import PDFRef from 'src/core/objects/PDFRef';
import PDFAcroField from 'src/core/acroform/PDFAcroField';
import PDFAcroTerminal from 'src/core/acroform/PDFAcroTerminal';
import PDFAcroNonTerminal from 'src/core/acroform/PDFAcroNonTerminal';
import PDFAcroButton from 'src/core/acroform/PDFAcroButton';
import PDFAcroSignature from 'src/core/acroform/PDFAcroSignature';
import PDFAcroChoice from 'src/core/acroform/PDFAcroChoice';
import PDFAcroText from 'src/core/acroform/PDFAcroText';
import PDFAcroPushButton from 'src/core/acroform/PDFAcroPushButton';
import PDFAcroRadioButton from 'src/core/acroform/PDFAcroRadioButton';
import PDFAcroCheckBox from 'src/core/acroform/PDFAcroCheckBox';
import PDFAcroComboBox from 'src/core/acroform/PDFAcroComboBox';
import PDFAcroListBox from 'src/core/acroform/PDFAcroListBox';
import { AcroButtonFlags, AcroChoiceFlags } from 'src/core/acroform/flags';
export const createPDFAcroFields = (
kidDicts?: PDFArray,
): [PDFAcroField, PDFRef][] => {
if (!kidDicts) return [];
const kids: [PDFAcroField, PDFRef][] = [];
for (let idx = 0, len = kidDicts.size(); idx < len; idx++) {
const ref = kidDicts.get(idx);
const dict = kidDicts.lookup(idx);
// if (dict instanceof PDFDict) kids.push(PDFAcroField.fromDict(dict));
if (ref instanceof PDFRef && dict instanceof PDFDict) {
kids.push([createPDFAcroField(dict, ref), ref]);
}
}
return kids;
};
export const createPDFAcroField = (
dict: PDFDict,
ref: PDFRef,
): PDFAcroField => {
const isNonTerminal = isNonTerminalAcroField(dict);
if (isNonTerminal) return PDFAcroNonTerminal.fromDict(dict, ref);
return createPDFAcroTerminal(dict, ref);
};
// TODO: Maybe just check if the dict is *not* a widget? That might be better.
// According to the PDF spec:
//
// > A field's children in the hierarchy may also include widget annotations
// > that define its appearance on the page. A field that has children that
// > are fields is called a non-terminal field. A field that does not have
// > children that are fields is called a terminal field.
//
// The spec is not entirely clear about how to determine whether a given
// dictionary represents an acrofield or a widget annotation. So we will assume
// that a dictionary is an acrofield if it is a member of the `/Kids` array
// and it contains a `/T` entry (widgets do not have `/T` entries). This isn't
// a bullet proof solution, because the `/T` entry is technically defined as
// optional for acrofields by the PDF spec. But in practice all acrofields seem
// to have a `/T` entry defined.
const isNonTerminalAcroField = (dict: PDFDict): boolean => {
const kids = dict.lookup(PDFName.of('Kids'));
if (kids instanceof PDFArray) {
for (let idx = 0, len = kids.size(); idx < len; idx++) {
const kid = kids.lookup(idx);
const kidIsField = kid instanceof PDFDict && kid.has(PDFName.of('T'));
if (kidIsField) return true;
}
}
return false;
};
const createPDFAcroTerminal = (dict: PDFDict, ref: PDFRef): PDFAcroTerminal => {
const ftNameOrRef = getInheritableAttribute(dict, PDFName.of('FT'));
const type = dict.context.lookup(ftNameOrRef, PDFName);
if (type === PDFName.of('Btn')) return createPDFAcroButton(dict, ref);
if (type === PDFName.of('Ch')) return createPDFAcroChoice(dict, ref);
if (type === PDFName.of('Tx')) return PDFAcroText.fromDict(dict, ref);
if (type === PDFName.of('Sig')) return PDFAcroSignature.fromDict(dict, ref);
// We should never reach this line. But there are a lot of weird PDFs out
// there. So, just to be safe, we'll try to handle things gracefully instead
// of throwing an error.
return PDFAcroTerminal.fromDict(dict, ref);
};
const createPDFAcroButton = (dict: PDFDict, ref: PDFRef): PDFAcroButton => {
const ffNumberOrRef = getInheritableAttribute(dict, PDFName.of('Ff'));
const ffNumber = dict.context.lookupMaybe(ffNumberOrRef, PDFNumber);
const flags = ffNumber?.asNumber() ?? 0;
if (flagIsSet(flags, AcroButtonFlags.PushButton)) {
return PDFAcroPushButton.fromDict(dict, ref);
} else if (flagIsSet(flags, AcroButtonFlags.Radio)) {
return PDFAcroRadioButton.fromDict(dict, ref);
} else {
return PDFAcroCheckBox.fromDict(dict, ref);
}
};
const createPDFAcroChoice = (dict: PDFDict, ref: PDFRef): PDFAcroChoice => {
const ffNumberOrRef = getInheritableAttribute(dict, PDFName.of('Ff'));
const ffNumber = dict.context.lookupMaybe(ffNumberOrRef, PDFNumber);
const flags = ffNumber?.asNumber() ?? 0;
if (flagIsSet(flags, AcroChoiceFlags.Combo)) {
return PDFAcroComboBox.fromDict(dict, ref);
} else {
return PDFAcroListBox.fromDict(dict, ref);
}
};
const flagIsSet = (flags: number, flag: number): boolean =>
(flags & flag) !== 0;
const getInheritableAttribute = (startNode: PDFDict, name: PDFName) => {
let attribute: PDFObject | undefined;
ascend(startNode, (node) => {
if (!attribute) attribute = node.get(name);
});
return attribute;
};
const ascend = (startNode: PDFDict, visitor: (node: PDFDict) => any) => {
visitor(startNode);
const Parent = startNode.lookupMaybe(PDFName.of('Parent'), PDFDict);
if (Parent) ascend(Parent, visitor);
};

View File

@@ -0,0 +1,133 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFArray from 'src/core/objects/PDFArray';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFString from 'src/core/objects/PDFString';
class AppearanceCharacteristics {
readonly dict: PDFDict;
static fromDict = (dict: PDFDict): AppearanceCharacteristics =>
new AppearanceCharacteristics(dict);
protected constructor(dict: PDFDict) {
this.dict = dict;
}
R(): PDFNumber | undefined {
const R = this.dict.lookup(PDFName.of('R'));
if (R instanceof PDFNumber) return R;
return undefined;
}
BC(): PDFArray | undefined {
const BC = this.dict.lookup(PDFName.of('BC'));
if (BC instanceof PDFArray) return BC;
return undefined;
}
BG(): PDFArray | undefined {
const BG = this.dict.lookup(PDFName.of('BG'));
if (BG instanceof PDFArray) return BG;
return undefined;
}
CA(): PDFHexString | PDFString | undefined {
const CA = this.dict.lookup(PDFName.of('CA'));
if (CA instanceof PDFHexString || CA instanceof PDFString) return CA;
return undefined;
}
RC(): PDFHexString | PDFString | undefined {
const RC = this.dict.lookup(PDFName.of('RC'));
if (RC instanceof PDFHexString || RC instanceof PDFString) return RC;
return undefined;
}
AC(): PDFHexString | PDFString | undefined {
const AC = this.dict.lookup(PDFName.of('AC'));
if (AC instanceof PDFHexString || AC instanceof PDFString) return AC;
return undefined;
}
getRotation(): number | undefined {
return this.R()?.asNumber();
}
getBorderColor(): number[] | undefined {
const BC = this.BC();
if (!BC) return undefined;
const components: number[] = [];
for (let idx = 0, len = BC?.size(); idx < len; idx++) {
const component = BC.get(idx);
if (component instanceof PDFNumber) components.push(component.asNumber());
}
return components;
}
getBackgroundColor(): number[] | undefined {
const BG = this.BG();
if (!BG) return undefined;
const components: number[] = [];
for (let idx = 0, len = BG?.size(); idx < len; idx++) {
const component = BG.get(idx);
if (component instanceof PDFNumber) components.push(component.asNumber());
}
return components;
}
getCaptions(): { normal?: string; rollover?: string; down?: string } {
const CA = this.CA();
const RC = this.RC();
const AC = this.AC();
return {
normal: CA?.decodeText(),
rollover: RC?.decodeText(),
down: AC?.decodeText(),
};
}
setRotation(rotation: number) {
const R = this.dict.context.obj(rotation);
this.dict.set(PDFName.of('R'), R);
}
setBorderColor(color: number[]) {
const BC = this.dict.context.obj(color);
this.dict.set(PDFName.of('BC'), BC);
}
setBackgroundColor(color: number[]) {
const BG = this.dict.context.obj(color);
this.dict.set(PDFName.of('BG'), BG);
}
setCaptions(captions: { normal: string; rollover?: string; down?: string }) {
const CA = PDFHexString.fromText(captions.normal);
this.dict.set(PDFName.of('CA'), CA);
if (captions.rollover) {
const RC = PDFHexString.fromText(captions.rollover);
this.dict.set(PDFName.of('RC'), RC);
} else {
this.dict.delete(PDFName.of('RC'));
}
if (captions.down) {
const AC = PDFHexString.fromText(captions.down);
this.dict.set(PDFName.of('AC'), AC);
} else {
this.dict.delete(PDFName.of('AC'));
}
}
}
export default AppearanceCharacteristics;

View File

@@ -0,0 +1,31 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
// TODO: Also handle the `/S` and `/D` entries
class BorderStyle {
readonly dict: PDFDict;
static fromDict = (dict: PDFDict): BorderStyle => new BorderStyle(dict);
protected constructor(dict: PDFDict) {
this.dict = dict;
}
W(): PDFNumber | undefined {
const W = this.dict.lookup(PDFName.of('W'));
if (W instanceof PDFNumber) return W;
return undefined;
}
getWidth(): number | undefined {
return this.W()?.asNumber() ?? 1;
}
setWidth(width: number) {
const W = this.dict.context.obj(width);
this.dict.set(PDFName.of('W'), W);
}
}
export default BorderStyle;

View File

@@ -0,0 +1,148 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFStream from 'src/core/objects/PDFStream';
import PDFArray from 'src/core/objects/PDFArray';
import PDFRef from 'src/core/objects/PDFRef';
import PDFNumber from 'src/core/objects/PDFNumber';
class PDFAnnotation {
readonly dict: PDFDict;
static fromDict = (dict: PDFDict): PDFAnnotation => new PDFAnnotation(dict);
protected constructor(dict: PDFDict) {
this.dict = dict;
}
// This is technically required by the PDF spec
Rect(): PDFArray | undefined {
return this.dict.lookup(PDFName.of('Rect'), PDFArray);
}
AP(): PDFDict | undefined {
return this.dict.lookupMaybe(PDFName.of('AP'), PDFDict);
}
F(): PDFNumber | undefined {
const numberOrRef = this.dict.lookup(PDFName.of('F'));
return this.dict.context.lookupMaybe(numberOrRef, PDFNumber);
}
getRectangle(): { x: number; y: number; width: number; height: number } {
const Rect = this.Rect();
return Rect?.asRectangle() ?? { x: 0, y: 0, width: 0, height: 0 };
}
setRectangle(rect: { x: number; y: number; width: number; height: number }) {
const { x, y, width, height } = rect;
const Rect = this.dict.context.obj([x, y, x + width, y + height]);
this.dict.set(PDFName.of('Rect'), Rect);
}
getAppearanceState(): PDFName | undefined {
const AS = this.dict.lookup(PDFName.of('AS'));
if (AS instanceof PDFName) return AS;
return undefined;
}
setAppearanceState(state: PDFName) {
this.dict.set(PDFName.of('AS'), state);
}
setAppearances(appearances: PDFDict) {
this.dict.set(PDFName.of('AP'), appearances);
}
ensureAP(): PDFDict {
let AP = this.AP();
if (!AP) {
AP = this.dict.context.obj({});
this.dict.set(PDFName.of('AP'), AP);
}
return AP;
}
getNormalAppearance(): PDFRef | PDFDict {
const AP = this.ensureAP();
const N = AP.get(PDFName.of('N'));
if (N instanceof PDFRef || N instanceof PDFDict) return N;
throw new Error(`Unexpected N type: ${N?.constructor.name}`);
}
/** @param appearance A PDFDict or PDFStream (direct or ref) */
setNormalAppearance(appearance: PDFRef | PDFDict) {
const AP = this.ensureAP();
AP.set(PDFName.of('N'), appearance);
}
/** @param appearance A PDFDict or PDFStream (direct or ref) */
setRolloverAppearance(appearance: PDFRef | PDFDict) {
const AP = this.ensureAP();
AP.set(PDFName.of('R'), appearance);
}
/** @param appearance A PDFDict or PDFStream (direct or ref) */
setDownAppearance(appearance: PDFRef | PDFDict) {
const AP = this.ensureAP();
AP.set(PDFName.of('D'), appearance);
}
removeRolloverAppearance() {
const AP = this.AP();
AP?.delete(PDFName.of('R'));
}
removeDownAppearance() {
const AP = this.AP();
AP?.delete(PDFName.of('D'));
}
getAppearances():
| {
normal: PDFStream | PDFDict;
rollover?: PDFStream | PDFDict;
down?: PDFStream | PDFDict;
}
| undefined {
const AP = this.AP();
if (!AP) return undefined;
const N = AP.lookup(PDFName.of('N'), PDFDict, PDFStream);
const R = AP.lookupMaybe(PDFName.of('R'), PDFDict, PDFStream);
const D = AP.lookupMaybe(PDFName.of('D'), PDFDict, PDFStream);
return { normal: N, rollover: R, down: D };
}
getFlags(): number {
return this.F()?.asNumber() ?? 0;
}
setFlags(flags: number) {
this.dict.set(PDFName.of('F'), PDFNumber.of(flags));
}
hasFlag(flag: number): boolean {
const flags = this.getFlags();
return (flags & flag) !== 0;
}
setFlag(flag: number) {
const flags = this.getFlags();
this.setFlags(flags | flag);
}
clearFlag(flag: number) {
const flags = this.getFlags();
this.setFlags(flags & ~flag);
}
setFlagTo(flag: number, enable: boolean) {
if (enable) this.setFlag(flag);
else this.clearFlag(flag);
}
}
export default PDFAnnotation;

View File

@@ -0,0 +1,112 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFRef from 'src/core/objects/PDFRef';
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFContext from 'src/core/PDFContext';
import BorderStyle from 'src/core/annotation/BorderStyle';
import PDFAnnotation from 'src/core/annotation/PDFAnnotation';
import AppearanceCharacteristics from 'src/core/annotation/AppearanceCharacteristics';
class PDFWidgetAnnotation extends PDFAnnotation {
static fromDict = (dict: PDFDict): PDFWidgetAnnotation =>
new PDFWidgetAnnotation(dict);
static create = (context: PDFContext, parent: PDFRef) => {
const dict = context.obj({
Type: 'Annot',
Subtype: 'Widget',
Rect: [0, 0, 0, 0],
Parent: parent,
});
return new PDFWidgetAnnotation(dict);
};
MK(): PDFDict | undefined {
const MK = this.dict.lookup(PDFName.of('MK'));
if (MK instanceof PDFDict) return MK;
return undefined;
}
BS(): PDFDict | undefined {
const BS = this.dict.lookup(PDFName.of('BS'));
if (BS instanceof PDFDict) return BS;
return undefined;
}
DA(): PDFString | PDFHexString | undefined {
const da = this.dict.lookup(PDFName.of('DA'));
if (da instanceof PDFString || da instanceof PDFHexString) return da;
return undefined;
}
P(): PDFRef | undefined {
const P = this.dict.get(PDFName.of('P'));
if (P instanceof PDFRef) return P;
return undefined;
}
setP(page: PDFRef) {
this.dict.set(PDFName.of('P'), page);
}
setDefaultAppearance(appearance: string) {
this.dict.set(PDFName.of('DA'), PDFString.of(appearance));
}
getDefaultAppearance(): string | undefined {
const DA = this.DA();
if (DA instanceof PDFHexString) {
return DA.decodeText();
}
return DA?.asString();
}
getAppearanceCharacteristics(): AppearanceCharacteristics | undefined {
const MK = this.MK();
if (MK) return AppearanceCharacteristics.fromDict(MK);
return undefined;
}
getOrCreateAppearanceCharacteristics(): AppearanceCharacteristics {
const MK = this.MK();
if (MK) return AppearanceCharacteristics.fromDict(MK);
const ac = AppearanceCharacteristics.fromDict(this.dict.context.obj({}));
this.dict.set(PDFName.of('MK'), ac.dict);
return ac;
}
getBorderStyle(): BorderStyle | undefined {
const BS = this.BS();
if (BS) return BorderStyle.fromDict(BS);
return undefined;
}
getOrCreateBorderStyle(): BorderStyle {
const BS = this.BS();
if (BS) return BorderStyle.fromDict(BS);
const bs = BorderStyle.fromDict(this.dict.context.obj({}));
this.dict.set(PDFName.of('BS'), bs.dict);
return bs;
}
getOnValue(): PDFName | undefined {
const normal = this.getAppearances()?.normal;
if (normal instanceof PDFDict) {
const keys = normal.keys();
for (let idx = 0, len = keys.length; idx < len; idx++) {
const key = keys[idx];
if (key !== PDFName.of('Off')) return key;
}
}
return undefined;
}
}
export default PDFWidgetAnnotation;

90
node_modules/pdf-lib/src/core/annotation/flags.ts generated vendored Normal file
View File

@@ -0,0 +1,90 @@
const flag = (bitIndex: number) => 1 << bitIndex;
/** From PDF spec table 165 */
export enum AnnotationFlags {
/**
* If set, do not display the annotation if it does not belong to one of the
* standard annotation types and no annotation handler is available. If clear,
* display such an unknown annotation using an appearance stream specified by
* its appearance dictionary, if any.
*/
Invisible = flag(1 - 1),
/**
* If set, do not display or print the annotation or allow it to interact with
* the user, regardless of its annotation type or whether an annotation
* handler is available.
*
* In cases where screen space is limited, the ability to hide and show
* annotations selectively can be used in combination with appearance streams
* to display auxiliary pop-up information similar in function to online help
* systems.
*/
Hidden = flag(2 - 1),
/**
* If set, print the annotation when the page is printed. If clear, never
* print the annotation, regardless of whether it is displayed on the screen.
*
* This can be useful for annotations representing interactive pushbuttons,
* which would serve no meaningful purpose on the printed page.
*/
Print = flag(3 - 1),
/**
* If set, do not scale the annotations appearance to match the magnification
* of the page. The location of the annotation on the page (defined by the
* upper-left corner of its annotation rectangle) shall remain fixed,
* regardless of the page magnification.
*/
NoZoom = flag(4 - 1),
/**
* If set, do not rotate the annotations appearance to match the rotation of
* the page. The upper-left corner of the annotation rectangle shall remain in
* a fixed location on the page, regardless of the page rotation.
*/
NoRotate = flag(5 - 1),
/**
* If set, do not display the annotation on the screen or allow it to interact
* with the user. The annotation may be printed (depending on the setting of
* the Print flag) but should be considered hidden for purposes of on-screen
* display and user interaction.
*/
NoView = flag(6 - 1),
/**
* If set, do not allow the annotation to interact with the user. The
* annotation may be displayed or printed (depending on the settings of the
* NoView and Print flags) but should not respond to mouse clicks or change
* its appearance in response to mouse motions.
*
* This flag shall be ignored for widget annotations; its function is
* subsumed by the ReadOnly flag of the associated form field.
*/
ReadOnly = flag(7 - 1),
/**
* If set, do not allow the annotation to be deleted or its properties
* (including position and size) to be modified by the user. However, this
* flag does not restrict changes to the annotations contents, such as the
* value of a form field.
*/
Locked = flag(8 - 1),
/**
* If set, invert the interpretation of the NoView flag for certain events.
*
* A typical use is to have an annotation that appears only when a mouse
* cursor is held over it.
*/
ToggleNoView = flag(9 - 1),
/**
* If set, do not allow the contents of the annotation to be modified by the
* user. This flag does not restrict deletion of the annotation or changes to
* other annotation properties, such as position and size.
*/
LockedContents = flag(10 - 1),
}

4
node_modules/pdf-lib/src/core/annotation/index.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
export { default as PDFAnnotation } from 'src/core/annotation/PDFAnnotation';
export { default as PDFWidgetAnnotation } from 'src/core/annotation/PDFWidgetAnnotation';
export { default as AppearanceCharacteristics } from 'src/core/annotation/AppearanceCharacteristics';
export * from 'src/core/annotation/flags';

View File

@@ -0,0 +1,173 @@
import PDFRef from 'src/core/objects/PDFRef';
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer, padStart } from 'src/utils';
export interface Entry {
ref: PDFRef;
offset: number;
deleted: boolean;
}
/**
* Entries should be added using the [[addEntry]] and [[addDeletedEntry]]
* methods **in order of ascending object number**.
*/
class PDFCrossRefSection {
static create = () =>
new PDFCrossRefSection({
ref: PDFRef.of(0, 65535),
offset: 0,
deleted: true,
});
static createEmpty = () => new PDFCrossRefSection();
private subsections: Entry[][];
private chunkIdx: number;
private chunkLength: number;
private constructor(firstEntry: Entry | void) {
this.subsections = firstEntry ? [[firstEntry]] : [];
this.chunkIdx = 0;
this.chunkLength = firstEntry ? 1 : 0;
}
addEntry(ref: PDFRef, offset: number): void {
this.append({ ref, offset, deleted: false });
}
addDeletedEntry(ref: PDFRef, nextFreeObjectNumber: number): void {
this.append({ ref, offset: nextFreeObjectNumber, deleted: true });
}
toString(): string {
let section = `xref\n`;
for (
let rangeIdx = 0, rangeLen = this.subsections.length;
rangeIdx < rangeLen;
rangeIdx++
) {
const range = this.subsections[rangeIdx];
section += `${range[0].ref.objectNumber} ${range.length}\n`;
for (
let entryIdx = 0, entryLen = range.length;
entryIdx < entryLen;
entryIdx++
) {
const entry = range[entryIdx];
section += padStart(String(entry.offset), 10, '0');
section += ' ';
section += padStart(String(entry.ref.generationNumber), 5, '0');
section += ' ';
section += entry.deleted ? 'f' : 'n';
section += ' \n';
}
}
return section;
}
sizeInBytes(): number {
let size = 5;
for (let idx = 0, len = this.subsections.length; idx < len; idx++) {
const subsection = this.subsections[idx];
const subsectionLength = subsection.length;
const [firstEntry] = subsection;
size += 2;
size += String(firstEntry.ref.objectNumber).length;
size += String(subsectionLength).length;
size += 20 * subsectionLength;
}
return size;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.x;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.f;
buffer[offset++] = CharCodes.Newline;
offset += this.copySubsectionsIntoBuffer(this.subsections, buffer, offset);
return offset - initialOffset;
}
private copySubsectionsIntoBuffer(
subsections: Entry[][],
buffer: Uint8Array,
offset: number,
): number {
const initialOffset = offset;
const length = subsections.length;
for (let idx = 0; idx < length; idx++) {
const subsection = this.subsections[idx];
const firstObjectNumber = String(subsection[0].ref.objectNumber);
offset += copyStringIntoBuffer(firstObjectNumber, buffer, offset);
buffer[offset++] = CharCodes.Space;
const rangeLength = String(subsection.length);
offset += copyStringIntoBuffer(rangeLength, buffer, offset);
buffer[offset++] = CharCodes.Newline;
offset += this.copyEntriesIntoBuffer(subsection, buffer, offset);
}
return offset - initialOffset;
}
private copyEntriesIntoBuffer(
entries: Entry[],
buffer: Uint8Array,
offset: number,
): number {
const length = entries.length;
for (let idx = 0; idx < length; idx++) {
const entry = entries[idx];
const entryOffset = padStart(String(entry.offset), 10, '0');
offset += copyStringIntoBuffer(entryOffset, buffer, offset);
buffer[offset++] = CharCodes.Space;
const entryGen = padStart(String(entry.ref.generationNumber), 5, '0');
offset += copyStringIntoBuffer(entryGen, buffer, offset);
buffer[offset++] = CharCodes.Space;
buffer[offset++] = entry.deleted ? CharCodes.f : CharCodes.n;
buffer[offset++] = CharCodes.Space;
buffer[offset++] = CharCodes.Newline;
}
return 20 * length;
}
private append(currEntry: Entry): void {
if (this.chunkLength === 0) {
this.subsections.push([currEntry]);
this.chunkIdx = 0;
this.chunkLength = 1;
return;
}
const chunk = this.subsections[this.chunkIdx];
const prevEntry = chunk[this.chunkLength - 1];
if (currEntry.ref.objectNumber - prevEntry.ref.objectNumber > 1) {
this.subsections.push([currEntry]);
this.chunkIdx += 1;
this.chunkLength = 1;
} else {
chunk.push(currEntry);
this.chunkLength += 1;
}
}
}
export default PDFCrossRefSection;

49
node_modules/pdf-lib/src/core/document/PDFHeader.ts generated vendored Normal file
View File

@@ -0,0 +1,49 @@
import CharCodes from 'src/core/syntax/CharCodes';
import { charFromCode, copyStringIntoBuffer } from 'src/utils';
class PDFHeader {
static forVersion = (major: number, minor: number) =>
new PDFHeader(major, minor);
private readonly major: string;
private readonly minor: string;
private constructor(major: number, minor: number) {
this.major = String(major);
this.minor = String(minor);
}
toString(): string {
const bc = charFromCode(129);
return `%PDF-${this.major}.${this.minor}\n%${bc}${bc}${bc}${bc}`;
}
sizeInBytes(): number {
return 12 + this.major.length + this.minor.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.Percent;
buffer[offset++] = CharCodes.P;
buffer[offset++] = CharCodes.D;
buffer[offset++] = CharCodes.F;
buffer[offset++] = CharCodes.Dash;
offset += copyStringIntoBuffer(this.major, buffer, offset);
buffer[offset++] = CharCodes.Period;
offset += copyStringIntoBuffer(this.minor, buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.Percent;
buffer[offset++] = 129;
buffer[offset++] = 129;
buffer[offset++] = 129;
buffer[offset++] = 129;
return offset - initialOffset;
}
}
export default PDFHeader;

49
node_modules/pdf-lib/src/core/document/PDFTrailer.ts generated vendored Normal file
View File

@@ -0,0 +1,49 @@
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer } from 'src/utils';
class PDFTrailer {
static forLastCrossRefSectionOffset = (offset: number) =>
new PDFTrailer(offset);
private readonly lastXRefOffset: string;
private constructor(lastXRefOffset: number) {
this.lastXRefOffset = String(lastXRefOffset);
}
toString(): string {
return `startxref\n${this.lastXRefOffset}\n%%EOF`;
}
sizeInBytes(): number {
return 16 + this.lastXRefOffset.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.s;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.x;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.f;
buffer[offset++] = CharCodes.Newline;
offset += copyStringIntoBuffer(this.lastXRefOffset, buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.Percent;
buffer[offset++] = CharCodes.Percent;
buffer[offset++] = CharCodes.E;
buffer[offset++] = CharCodes.O;
buffer[offset++] = CharCodes.F;
return offset - initialOffset;
}
}
export default PDFTrailer;

View File

@@ -0,0 +1,39 @@
import PDFDict from 'src/core/objects/PDFDict';
import CharCodes from 'src/core/syntax/CharCodes';
class PDFTrailerDict {
static of = (dict: PDFDict) => new PDFTrailerDict(dict);
readonly dict: PDFDict;
private constructor(dict: PDFDict) {
this.dict = dict;
}
toString(): string {
return `trailer\n${this.dict.toString()}`;
}
sizeInBytes(): number {
return 8 + this.dict.sizeInBytes();
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.i;
buffer[offset++] = CharCodes.l;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.Newline;
offset += this.dict.copyBytesInto(buffer, offset);
return offset - initialOffset;
}
}
export default PDFTrailerDict;

70
node_modules/pdf-lib/src/core/embedders/CMap.ts generated vendored Normal file
View File

@@ -0,0 +1,70 @@
import { Glyph } from 'src/types/fontkit';
import { toHexString, toHexStringOfMinLength } from 'src/utils';
import {
hasSurrogates,
highSurrogate,
isWithinBMP,
lowSurrogate,
} from 'src/utils/unicode';
/** [fontId, codePoint] */
type BfChar = [string, string];
/** `glyphs` should be an array of unique glyphs */
export const createCmap = (glyphs: Glyph[], glyphId: (g?: Glyph) => number) => {
const bfChars: BfChar[] = new Array(glyphs.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const glyph = glyphs[idx];
const id = cmapHexFormat(cmapHexString(glyphId(glyph)));
const unicode = cmapHexFormat(...glyph.codePoints.map(cmapCodePointFormat));
bfChars[idx] = [id, unicode];
}
return fillCmapTemplate(bfChars);
};
/* =============================== Templates ================================ */
const fillCmapTemplate = (bfChars: BfChar[]) => `\
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (UCS)
/Supplement 0
>> def
/CMapName /Adobe-Identity-UCS def
/CMapType 2 def
1 begincodespacerange
<0000><ffff>
endcodespacerange
${bfChars.length} beginbfchar
${bfChars.map(([glyphId, codePoint]) => `${glyphId} ${codePoint}`).join('\n')}
endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end\
`;
/* =============================== Utilities ================================ */
const cmapHexFormat = (...values: string[]) => `<${values.join('')}>`;
const cmapHexString = (value: number) => toHexStringOfMinLength(value, 4);
const cmapCodePointFormat = (codePoint: number) => {
if (isWithinBMP(codePoint)) return cmapHexString(codePoint);
if (hasSurrogates(codePoint)) {
const hs = highSurrogate(codePoint);
const ls = lowSurrogate(codePoint);
return `${cmapHexString(hs)}${cmapHexString(ls)}`;
}
const hex = toHexString(codePoint);
const msg = `0x${hex} is not a valid UTF-8 or UTF-16 codepoint.`;
throw new Error(msg);
};

View File

@@ -0,0 +1,249 @@
import { Font, Fontkit, Glyph, TypeFeatures } from 'src/types/fontkit';
import { createCmap } from 'src/core/embedders/CMap';
import { deriveFontFlags } from 'src/core/embedders/FontFlags';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFRef from 'src/core/objects/PDFRef';
import PDFString from 'src/core/objects/PDFString';
import PDFContext from 'src/core/PDFContext';
import {
byAscendingId,
Cache,
sortedUniq,
toHexStringOfMinLength,
} from 'src/utils';
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/devongovett/pdfkit/blob/e71edab0dd4657b5a767804ba86c94c58d01fbca/lib/image/jpeg.coffee
*/
class CustomFontEmbedder {
static async for(
fontkit: Fontkit,
fontData: Uint8Array,
customName?: string,
fontFeatures?: TypeFeatures,
) {
const font = await fontkit.create(fontData);
return new CustomFontEmbedder(font, fontData, customName, fontFeatures);
}
readonly font: Font;
readonly scale: number;
readonly fontData: Uint8Array;
readonly fontName: string;
readonly customName: string | undefined;
readonly fontFeatures: TypeFeatures | undefined;
protected baseFontName: string;
protected glyphCache: Cache<Glyph[]>;
protected constructor(
font: Font,
fontData: Uint8Array,
customName?: string,
fontFeatures?: TypeFeatures,
) {
this.font = font;
this.scale = 1000 / this.font.unitsPerEm;
this.fontData = fontData;
this.fontName = this.font.postscriptName || 'Font';
this.customName = customName;
this.fontFeatures = fontFeatures;
this.baseFontName = '';
this.glyphCache = Cache.populatedBy(this.allGlyphsInFontSortedById);
}
/**
* Encode the JavaScript string into this font. (JavaScript encodes strings in
* Unicode, but embedded fonts use their own custom encodings)
*/
encodeText(text: string): PDFHexString {
const { glyphs } = this.font.layout(text, this.fontFeatures);
const hexCodes = new Array(glyphs.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
hexCodes[idx] = toHexStringOfMinLength(glyphs[idx].id, 4);
}
return PDFHexString.of(hexCodes.join(''));
}
// The advanceWidth takes into account kerning automatically, so we don't
// have to do that manually like we do for the standard fonts.
widthOfTextAtSize(text: string, size: number): number {
const { glyphs } = this.font.layout(text, this.fontFeatures);
let totalWidth = 0;
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
totalWidth += glyphs[idx].advanceWidth * this.scale;
}
const scale = size / 1000;
return totalWidth * scale;
}
heightOfFontAtSize(
size: number,
options: { descender?: boolean } = {},
): number {
const { descender = true } = options;
const { ascent, descent, bbox } = this.font;
const yTop = (ascent || bbox.maxY) * this.scale;
const yBottom = (descent || bbox.minY) * this.scale;
let height = yTop - yBottom;
if (!descender) height -= Math.abs(descent) || 0;
return (height / 1000) * size;
}
sizeOfFontAtHeight(height: number): number {
const { ascent, descent, bbox } = this.font;
const yTop = (ascent || bbox.maxY) * this.scale;
const yBottom = (descent || bbox.minY) * this.scale;
return (1000 * height) / (yTop - yBottom);
}
embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
this.baseFontName =
this.customName || context.addRandomSuffix(this.fontName);
return this.embedFontDict(context, ref);
}
protected async embedFontDict(
context: PDFContext,
ref?: PDFRef,
): Promise<PDFRef> {
const cidFontDictRef = await this.embedCIDFontDict(context);
const unicodeCMapRef = this.embedUnicodeCmap(context);
const fontDict = context.obj({
Type: 'Font',
Subtype: 'Type0',
BaseFont: this.baseFontName,
Encoding: 'Identity-H',
DescendantFonts: [cidFontDictRef],
ToUnicode: unicodeCMapRef,
});
if (ref) {
context.assign(ref, fontDict);
return ref;
} else {
return context.register(fontDict);
}
}
protected isCFF(): boolean {
return this.font.cff;
}
protected async embedCIDFontDict(context: PDFContext): Promise<PDFRef> {
const fontDescriptorRef = await this.embedFontDescriptor(context);
const cidFontDict = context.obj({
Type: 'Font',
Subtype: this.isCFF() ? 'CIDFontType0' : 'CIDFontType2',
CIDToGIDMap: 'Identity',
BaseFont: this.baseFontName,
CIDSystemInfo: {
Registry: PDFString.of('Adobe'),
Ordering: PDFString.of('Identity'),
Supplement: 0,
},
FontDescriptor: fontDescriptorRef,
W: this.computeWidths(),
});
return context.register(cidFontDict);
}
protected async embedFontDescriptor(context: PDFContext): Promise<PDFRef> {
const fontStreamRef = await this.embedFontStream(context);
const { scale } = this;
const { italicAngle, ascent, descent, capHeight, xHeight } = this.font;
const { minX, minY, maxX, maxY } = this.font.bbox;
const fontDescriptor = context.obj({
Type: 'FontDescriptor',
FontName: this.baseFontName,
Flags: deriveFontFlags(this.font),
FontBBox: [minX * scale, minY * scale, maxX * scale, maxY * scale],
ItalicAngle: italicAngle,
Ascent: ascent * scale,
Descent: descent * scale,
CapHeight: (capHeight || ascent) * scale,
XHeight: (xHeight || 0) * scale,
// Not sure how to compute/find this, nor is anybody else really:
// https://stackoverflow.com/questions/35485179/stemv-value-of-the-truetype-font
StemV: 0,
[this.isCFF() ? 'FontFile3' : 'FontFile2']: fontStreamRef,
});
return context.register(fontDescriptor);
}
protected async serializeFont(): Promise<Uint8Array> {
return this.fontData;
}
protected async embedFontStream(context: PDFContext): Promise<PDFRef> {
const fontStream = context.flateStream(await this.serializeFont(), {
Subtype: this.isCFF() ? 'CIDFontType0C' : undefined,
});
return context.register(fontStream);
}
protected embedUnicodeCmap(context: PDFContext): PDFRef {
const cmap = createCmap(this.glyphCache.access(), this.glyphId.bind(this));
const cmapStream = context.flateStream(cmap);
return context.register(cmapStream);
}
protected glyphId(glyph?: Glyph): number {
return glyph ? glyph.id : -1;
}
protected computeWidths(): (number | number[])[] {
const glyphs = this.glyphCache.access();
const widths: (number | number[])[] = [];
let currSection: number[] = [];
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const currGlyph = glyphs[idx];
const prevGlyph = glyphs[idx - 1];
const currGlyphId = this.glyphId(currGlyph);
const prevGlyphId = this.glyphId(prevGlyph);
if (idx === 0) {
widths.push(currGlyphId);
} else if (currGlyphId - prevGlyphId !== 1) {
widths.push(currSection);
widths.push(currGlyphId);
currSection = [];
}
currSection.push(currGlyph.advanceWidth * this.scale);
}
widths.push(currSection);
return widths;
}
private allGlyphsInFontSortedById = (): Glyph[] => {
const glyphs: Glyph[] = new Array(this.font.characterSet.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const codePoint = this.font.characterSet[idx];
glyphs[idx] = this.font.glyphForCodePoint(codePoint);
}
return sortedUniq(glyphs.sort(byAscendingId), (g) => g.id);
};
}
export default CustomFontEmbedder;

View File

@@ -0,0 +1,84 @@
import { Font, Fontkit, Glyph, Subset, TypeFeatures } from 'src/types/fontkit';
import CustomFontEmbedder from 'src/core/embedders/CustomFontEmbedder';
import PDFHexString from 'src/core/objects/PDFHexString';
import { Cache, mergeUint8Arrays, toHexStringOfMinLength } from 'src/utils';
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/devongovett/pdfkit/blob/e71edab0dd4657b5a767804ba86c94c58d01fbca/lib/image/jpeg.coffee
*/
class CustomFontSubsetEmbedder extends CustomFontEmbedder {
static async for(
fontkit: Fontkit,
fontData: Uint8Array,
customFontName?: string,
fontFeatures?: TypeFeatures,
) {
const font = await fontkit.create(fontData);
return new CustomFontSubsetEmbedder(
font,
fontData,
customFontName,
fontFeatures,
);
}
private readonly subset: Subset;
private readonly glyphs: Glyph[];
private readonly glyphIdMap: Map<number, number>;
private constructor(
font: Font,
fontData: Uint8Array,
customFontName?: string,
fontFeatures?: TypeFeatures,
) {
super(font, fontData, customFontName, fontFeatures);
this.subset = this.font.createSubset();
this.glyphs = [];
this.glyphCache = Cache.populatedBy(() => this.glyphs);
this.glyphIdMap = new Map();
}
encodeText(text: string): PDFHexString {
const { glyphs } = this.font.layout(text, this.fontFeatures);
const hexCodes = new Array(glyphs.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const glyph = glyphs[idx];
const subsetGlyphId = this.subset.includeGlyph(glyph);
this.glyphs[subsetGlyphId - 1] = glyph;
this.glyphIdMap.set(glyph.id, subsetGlyphId);
hexCodes[idx] = toHexStringOfMinLength(subsetGlyphId, 4);
}
this.glyphCache.invalidate();
return PDFHexString.of(hexCodes.join(''));
}
protected isCFF(): boolean {
return (this.subset as any).cff;
}
protected glyphId(glyph?: Glyph): number {
return glyph ? this.glyphIdMap.get(glyph.id)! : -1;
}
protected serializeFont(): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const parts: Uint8Array[] = [];
this.subset
.encodeStream()
.on('data', (bytes) => parts.push(bytes))
.on('end', () => resolve(mergeUint8Arrays(parts)))
.on('error' as any, (err) => reject(err));
});
}
}
export default CustomFontSubsetEmbedder;

View File

@@ -0,0 +1,95 @@
import PDFString from 'src/core/objects/PDFString';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
/**
* From the PDF-A3 specification, section **3.1. Requirements - General**.
* See:
* * https://www.pdfa.org/wp-content/uploads/2018/10/PDF20_AN002-AF.pdf
*/
export enum AFRelationship {
Source = 'Source',
Data = 'Data',
Alternative = 'Alternative',
Supplement = 'Supplement',
EncryptedPayload = 'EncryptedPayload',
FormData = 'EncryptedPayload',
Schema = 'Schema',
Unspecified = 'Unspecified',
}
export interface EmbeddedFileOptions {
mimeType?: string;
description?: string;
creationDate?: Date;
modificationDate?: Date;
afRelationship?: AFRelationship;
}
class FileEmbedder {
static for(
bytes: Uint8Array,
fileName: string,
options: EmbeddedFileOptions = {},
) {
return new FileEmbedder(bytes, fileName, options);
}
private readonly fileData: Uint8Array;
readonly fileName: string;
readonly options: EmbeddedFileOptions;
private constructor(
fileData: Uint8Array,
fileName: string,
options: EmbeddedFileOptions = {},
) {
this.fileData = fileData;
this.fileName = fileName;
this.options = options;
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const {
mimeType,
description,
creationDate,
modificationDate,
afRelationship,
} = this.options;
const embeddedFileStream = context.flateStream(this.fileData, {
Type: 'EmbeddedFile',
Subtype: mimeType ?? undefined,
Params: {
Size: this.fileData.length,
CreationDate: creationDate
? PDFString.fromDate(creationDate)
: undefined,
ModDate: modificationDate
? PDFString.fromDate(modificationDate)
: undefined,
},
});
const embeddedFileStreamRef = context.register(embeddedFileStream);
const fileSpecDict = context.obj({
Type: 'Filespec',
F: PDFString.of(this.fileName), // TODO: Assert that this is plain ASCII
UF: PDFHexString.fromText(this.fileName),
EF: { F: embeddedFileStreamRef },
Desc: description ? PDFHexString.fromText(description) : undefined,
AFRelationship: afRelationship ?? undefined,
});
if (ref) {
context.assign(ref, fileSpecDict);
return ref;
} else {
return context.register(fileSpecDict);
}
}
}
export default FileEmbedder;

45
node_modules/pdf-lib/src/core/embedders/FontFlags.ts generated vendored Normal file
View File

@@ -0,0 +1,45 @@
import { Font } from 'src/types/fontkit';
export interface FontFlagOptions {
fixedPitch?: boolean;
serif?: boolean;
symbolic?: boolean;
script?: boolean;
nonsymbolic?: boolean;
italic?: boolean;
allCap?: boolean;
smallCap?: boolean;
forceBold?: boolean;
}
// prettier-ignore
const makeFontFlags = (options: FontFlagOptions) => {
let flags = 0;
const flipBit = (bit: number) => { flags |= (1 << (bit - 1)); };
if (options.fixedPitch) flipBit(1);
if (options.serif) flipBit(2);
if (options.symbolic) flipBit(3);
if (options.script) flipBit(4);
if (options.nonsymbolic) flipBit(6);
if (options.italic) flipBit(7);
if (options.allCap) flipBit(17);
if (options.smallCap) flipBit(18);
if (options.forceBold) flipBit(19);
return flags;
};
// From: https://github.com/foliojs/pdfkit/blob/83f5f7243172a017adcf6a7faa5547c55982c57b/lib/font/embedded.js#L123-L129
export const deriveFontFlags = (font: Font): number => {
const familyClass = font['OS/2'] ? font['OS/2'].sFamilyClass : 0;
const flags = makeFontFlags({
fixedPitch: font.post.isFixedPitch,
serif: 1 <= familyClass && familyClass <= 7,
symbolic: true, // Assume the font uses non-latin characters
script: familyClass === 10,
italic: font.head.macStyle.italic,
});
return flags;
};

View File

@@ -0,0 +1,34 @@
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFContext from 'src/core/PDFContext';
import PDFRef from 'src/core/objects/PDFRef';
class JavaScriptEmbedder {
static for(script: string, scriptName: string) {
return new JavaScriptEmbedder(script, scriptName);
}
private readonly script: string;
readonly scriptName: string;
private constructor(script: string, scriptName: string) {
this.script = script;
this.scriptName = scriptName;
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const jsActionDict = context.obj({
Type: 'Action',
S: 'JavaScript',
JS: PDFHexString.fromText(this.script),
});
if (ref) {
context.assign(ref, jsActionDict);
return ref;
} else {
return context.register(jsActionDict);
}
}
}
export default JavaScriptEmbedder;

127
node_modules/pdf-lib/src/core/embedders/JpegEmbedder.ts generated vendored Normal file
View File

@@ -0,0 +1,127 @@
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
// prettier-ignore
const MARKERS = [
0xffc0, 0xffc1, 0xffc2,
0xffc3, 0xffc5, 0xffc6,
0xffc7, 0xffc8, 0xffc9,
0xffca, 0xffcb, 0xffcc,
0xffcd, 0xffce, 0xffcf,
];
enum ColorSpace {
DeviceGray = 'DeviceGray',
DeviceRGB = 'DeviceRGB',
DeviceCMYK = 'DeviceCMYK',
}
const ChannelToColorSpace: { [idx: number]: ColorSpace | undefined } = {
1: ColorSpace.DeviceGray,
3: ColorSpace.DeviceRGB,
4: ColorSpace.DeviceCMYK,
};
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/foliojs/pdfkit/blob/a6af76467ce06bd6a2af4aa7271ccac9ff152a7d/lib/image/jpeg.js
*/
class JpegEmbedder {
static async for(imageData: Uint8Array) {
const dataView = new DataView(imageData.buffer);
const soi = dataView.getUint16(0);
if (soi !== 0xffd8) throw new Error('SOI not found in JPEG');
let pos = 2;
let marker: number;
while (pos < dataView.byteLength) {
marker = dataView.getUint16(pos);
pos += 2;
if (MARKERS.includes(marker)) break;
pos += dataView.getUint16(pos);
}
if (!MARKERS.includes(marker!)) throw new Error('Invalid JPEG');
pos += 2;
const bitsPerComponent = dataView.getUint8(pos++);
const height = dataView.getUint16(pos);
pos += 2;
const width = dataView.getUint16(pos);
pos += 2;
const channelByte = dataView.getUint8(pos++);
const channelName = ChannelToColorSpace[channelByte];
if (!channelName) throw new Error('Unknown JPEG channel.');
const colorSpace = channelName;
return new JpegEmbedder(
imageData,
bitsPerComponent,
width,
height,
colorSpace,
);
}
readonly bitsPerComponent: number;
readonly height: number;
readonly width: number;
readonly colorSpace: ColorSpace;
private readonly imageData: Uint8Array;
private constructor(
imageData: Uint8Array,
bitsPerComponent: number,
width: number,
height: number,
colorSpace: ColorSpace,
) {
this.imageData = imageData;
this.bitsPerComponent = bitsPerComponent;
this.width = width;
this.height = height;
this.colorSpace = colorSpace;
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const xObject = context.stream(this.imageData, {
Type: 'XObject',
Subtype: 'Image',
BitsPerComponent: this.bitsPerComponent,
Width: this.width,
Height: this.height,
ColorSpace: this.colorSpace,
Filter: 'DCTDecode',
// CMYK JPEG streams in PDF are typically stored complemented,
// with 1 as 'off' and 0 as 'on' (PDF 32000-1:2008, 8.6.4.4).
//
// Standalone CMYK JPEG (usually exported by Photoshop) are
// stored inverse, with 0 as 'off' and 1 as 'on', like RGB.
//
// Applying a swap here as a hedge that most bytes passing
// through this method will benefit from it.
Decode:
this.colorSpace === ColorSpace.DeviceCMYK
? [1, 0, 1, 0, 1, 0, 1, 0]
: undefined,
});
if (ref) {
context.assign(ref, xObject);
return ref;
} else {
return context.register(xObject);
}
}
}
export default JpegEmbedder;

View File

@@ -0,0 +1,141 @@
import {
MissingPageContentsEmbeddingError,
UnrecognizedStreamTypeError,
} from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFContext from 'src/core/PDFContext';
import { decodePDFRawStream } from 'src/core/streams/decode';
import PDFContentStream from 'src/core/structures/PDFContentStream';
import PDFPageLeaf from 'src/core/structures/PDFPageLeaf';
import CharCodes from 'src/core/syntax/CharCodes';
import { TransformationMatrix } from 'src/types/matrix';
import { mergeIntoTypedArray } from 'src/utils';
/**
* Represents a page bounding box.
* Usually `left` and `bottom` are 0 and right, top are equal
* to width, height if you want to clip to the whole page.
*
* y
* ^
* | +--------+ (width,height)
* | | |
* | | Page |
* | | |
* | | |
* (0,0) | +--------+
* +----------> x
*/
export interface PageBoundingBox {
left: number /** The left of the bounding box */;
bottom: number /** The bottom of the bounding box */;
right: number /** The right of the bounding box */;
top: number /** The top of the bounding box */;
}
const fullPageBoundingBox = (page: PDFPageLeaf) => {
const mediaBox = page.MediaBox();
const width =
mediaBox.lookup(2, PDFNumber).asNumber() -
mediaBox.lookup(0, PDFNumber).asNumber();
const height =
mediaBox.lookup(3, PDFNumber).asNumber() -
mediaBox.lookup(1, PDFNumber).asNumber();
return { left: 0, bottom: 0, right: width, top: height };
};
// Returns the identity matrix, modified to position the content of the given
// bounding box at (0, 0).
const boundingBoxAdjustedMatrix = (
bb: PageBoundingBox,
): TransformationMatrix => [1, 0, 0, 1, -bb.left, -bb.bottom];
class PDFPageEmbedder {
static async for(
page: PDFPageLeaf,
boundingBox?: PageBoundingBox,
transformationMatrix?: TransformationMatrix,
) {
return new PDFPageEmbedder(page, boundingBox, transformationMatrix);
}
readonly width: number;
readonly height: number;
readonly boundingBox: PageBoundingBox;
readonly transformationMatrix: TransformationMatrix;
private readonly page: PDFPageLeaf;
private constructor(
page: PDFPageLeaf,
boundingBox?: PageBoundingBox,
transformationMatrix?: TransformationMatrix,
) {
this.page = page;
const bb = boundingBox ?? fullPageBoundingBox(page);
this.width = bb.right - bb.left;
this.height = bb.top - bb.bottom;
this.boundingBox = bb;
this.transformationMatrix =
transformationMatrix ?? boundingBoxAdjustedMatrix(bb);
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const { Contents, Resources } = this.page.normalizedEntries();
if (!Contents) throw new MissingPageContentsEmbeddingError();
const decodedContents = this.decodeContents(Contents);
const { left, bottom, right, top } = this.boundingBox;
const xObject = context.flateStream(decodedContents, {
Type: 'XObject',
Subtype: 'Form',
FormType: 1,
BBox: [left, bottom, right, top],
Matrix: this.transformationMatrix,
Resources,
});
if (ref) {
context.assign(ref, xObject);
return ref;
} else {
return context.register(xObject);
}
}
// `contents` is an array of streams which are merged to include them in the XObject.
// This methods extracts each stream and joins them with a newline character.
private decodeContents(contents: PDFArray) {
const newline = Uint8Array.of(CharCodes.Newline);
const decodedContents: Uint8Array[] = [];
for (let idx = 0, len = contents.size(); idx < len; idx++) {
const stream = contents.lookup(idx, PDFStream);
let content: Uint8Array;
if (stream instanceof PDFRawStream) {
content = decodePDFRawStream(stream).decode();
} else if (stream instanceof PDFContentStream) {
content = stream.getUnencodedContents();
} else {
throw new UnrecognizedStreamTypeError(stream);
}
decodedContents.push(content, newline);
}
return mergeIntoTypedArray(...decodedContents);
}
}
export default PDFPageEmbedder;

69
node_modules/pdf-lib/src/core/embedders/PngEmbedder.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import { PNG } from 'src/utils/png';
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/devongovett/pdfkit/blob/e71edab0dd4657b5a767804ba86c94c58d01fbca/lib/image/png.coffee
*/
class PngEmbedder {
static async for(imageData: Uint8Array) {
const png = PNG.load(imageData);
return new PngEmbedder(png);
}
readonly bitsPerComponent: number;
readonly height: number;
readonly width: number;
readonly colorSpace: 'DeviceRGB';
private readonly image: PNG;
private constructor(png: PNG) {
this.image = png;
this.bitsPerComponent = png.bitsPerComponent;
this.width = png.width;
this.height = png.height;
this.colorSpace = 'DeviceRGB';
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const SMask = this.embedAlphaChannel(context);
const xObject = context.flateStream(this.image.rgbChannel, {
Type: 'XObject',
Subtype: 'Image',
BitsPerComponent: this.image.bitsPerComponent,
Width: this.image.width,
Height: this.image.height,
ColorSpace: this.colorSpace,
SMask,
});
if (ref) {
context.assign(ref, xObject);
return ref;
} else {
return context.register(xObject);
}
}
private embedAlphaChannel(context: PDFContext): PDFRef | undefined {
if (!this.image.alphaChannel) return undefined;
const xObject = context.flateStream(this.image.alphaChannel, {
Type: 'XObject',
Subtype: 'Image',
Height: this.image.height,
Width: this.image.width,
BitsPerComponent: this.image.bitsPerComponent,
ColorSpace: 'DeviceGray',
Decode: [0, 1],
});
return context.register(xObject);
}
}
export default PngEmbedder;

View File

@@ -0,0 +1,130 @@
import {
Encodings,
Font,
FontNames,
EncodingType,
} from '@pdf-lib/standard-fonts';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFRef from 'src/core/objects/PDFRef';
import PDFContext from 'src/core/PDFContext';
import { toCodePoint, toHexString } from 'src/utils';
export interface Glyph {
code: number;
name: string;
}
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/foliojs/pdfkit/blob/f91bdd61c164a72ea06be1a43dc0a412afc3925f/lib/font/afm.coffee
*/
class StandardFontEmbedder {
static for = (fontName: FontNames, customName?: string) =>
new StandardFontEmbedder(fontName, customName);
readonly font: Font;
readonly encoding: EncodingType;
readonly fontName: string;
readonly customName: string | undefined;
private constructor(fontName: FontNames, customName?: string) {
// prettier-ignore
this.encoding = (
fontName === FontNames.ZapfDingbats ? Encodings.ZapfDingbats
: fontName === FontNames.Symbol ? Encodings.Symbol
: Encodings.WinAnsi
);
this.font = Font.load(fontName);
this.fontName = this.font.FontName;
this.customName = customName;
}
/**
* Encode the JavaScript string into this font. (JavaScript encodes strings in
* Unicode, but standard fonts use either WinAnsi, ZapfDingbats, or Symbol
* encodings)
*/
encodeText(text: string): PDFHexString {
const glyphs = this.encodeTextAsGlyphs(text);
const hexCodes = new Array(glyphs.length);
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
hexCodes[idx] = toHexString(glyphs[idx].code);
}
return PDFHexString.of(hexCodes.join(''));
}
widthOfTextAtSize(text: string, size: number): number {
const glyphs = this.encodeTextAsGlyphs(text);
let totalWidth = 0;
for (let idx = 0, len = glyphs.length; idx < len; idx++) {
const left = glyphs[idx].name;
const right = (glyphs[idx + 1] || {}).name;
const kernAmount = this.font.getXAxisKerningForPair(left, right) || 0;
totalWidth += this.widthOfGlyph(left) + kernAmount;
}
const scale = size / 1000;
return totalWidth * scale;
}
heightOfFontAtSize(
size: number,
options: { descender?: boolean } = {},
): number {
const { descender = true } = options;
const { Ascender, Descender, FontBBox } = this.font;
const yTop = Ascender || FontBBox[3];
const yBottom = Descender || FontBBox[1];
let height = yTop - yBottom;
if (!descender) height += Descender || 0;
return (height / 1000) * size;
}
sizeOfFontAtHeight(height: number): number {
const { Ascender, Descender, FontBBox } = this.font;
const yTop = Ascender || FontBBox[3];
const yBottom = Descender || FontBBox[1];
return (1000 * height) / (yTop - yBottom);
}
embedIntoContext(context: PDFContext, ref?: PDFRef): PDFRef {
const fontDict = context.obj({
Type: 'Font',
Subtype: 'Type1',
BaseFont: this.customName || this.fontName,
Encoding:
this.encoding === Encodings.WinAnsi ? 'WinAnsiEncoding' : undefined,
});
if (ref) {
context.assign(ref, fontDict);
return ref;
} else {
return context.register(fontDict);
}
}
private widthOfGlyph(glyphName: string): number {
// Default to 250 if font doesn't specify a width
return this.font.getWidthOfGlyph(glyphName) || 250;
}
private encodeTextAsGlyphs(text: string): Glyph[] {
const codePoints = Array.from(text);
const glyphs: Glyph[] = new Array(codePoints.length);
for (let idx = 0, len = codePoints.length; idx < len; idx++) {
const codePoint = toCodePoint(codePoints[idx])!;
glyphs[idx] = this.encoding.encodeUnicodeCodePoint(codePoint);
}
return glyphs;
}
}
export default StandardFontEmbedder;

221
node_modules/pdf-lib/src/core/errors.ts generated vendored Normal file
View File

@@ -0,0 +1,221 @@
// tslint:disable: max-classes-per-file
import PDFObject from 'src/core/objects/PDFObject';
import { arrayAsString } from 'src/utils';
export class MethodNotImplementedError extends Error {
constructor(className: string, methodName: string) {
const msg = `Method ${className}.${methodName}() not implemented`;
super(msg);
}
}
export class PrivateConstructorError extends Error {
constructor(className: string) {
const msg = `Cannot construct ${className} - it has a private constructor`;
super(msg);
}
}
export class UnexpectedObjectTypeError extends Error {
constructor(expected: any | any[], actual: any) {
const name = (t: any) => t?.name ?? t?.constructor?.name;
const expectedTypes = Array.isArray(expected)
? expected.map(name)
: [name(expected)];
const msg =
`Expected instance of ${expectedTypes.join(' or ')}, ` +
`but got instance of ${actual ? name(actual) : actual}`;
super(msg);
}
}
export class UnsupportedEncodingError extends Error {
constructor(encoding: string) {
const msg = `${encoding} stream encoding not supported`;
super(msg);
}
}
export class ReparseError extends Error {
constructor(className: string, methodName: string) {
const msg = `Cannot call ${className}.${methodName}() more than once`;
super(msg);
}
}
export class MissingCatalogError extends Error {
constructor(ref?: PDFObject) {
const msg = `Missing catalog (ref=${ref})`;
super(msg);
}
}
export class MissingPageContentsEmbeddingError extends Error {
constructor() {
const msg = `Can't embed page with missing Contents`;
super(msg);
}
}
export class UnrecognizedStreamTypeError extends Error {
constructor(stream: any) {
const streamType = stream?.contructor?.name ?? stream?.name ?? stream;
const msg = `Unrecognized stream type: ${streamType}`;
super(msg);
}
}
export class PageEmbeddingMismatchedContextError extends Error {
constructor() {
const msg = `Found mismatched contexts while embedding pages. All pages in the array passed to \`PDFDocument.embedPages()\` must be from the same document.`;
super(msg);
}
}
export class PDFArrayIsNotRectangleError extends Error {
constructor(size: number) {
const msg = `Attempted to convert PDFArray with ${size} elements to rectangle, but must have exactly 4 elements.`;
super(msg);
}
}
export class InvalidPDFDateStringError extends Error {
constructor(value: string) {
const msg = `Attempted to convert "${value}" to a date, but it does not match the PDF date string format.`;
super(msg);
}
}
export class InvalidTargetIndexError extends Error {
constructor(targetIndex: number, Count: number) {
const msg = `Invalid targetIndex specified: targetIndex=${targetIndex} must be less than Count=${Count}`;
super(msg);
}
}
export class CorruptPageTreeError extends Error {
constructor(targetIndex: number, operation: string) {
const msg = `Failed to ${operation} at targetIndex=${targetIndex} due to corrupt page tree: It is likely that one or more 'Count' entries are invalid`;
super(msg);
}
}
export class IndexOutOfBoundsError extends Error {
constructor(index: number, min: number, max: number) {
const msg = `index should be at least ${min} and at most ${max}, but was actually ${index}`;
super(msg);
}
}
export class InvalidAcroFieldValueError extends Error {
constructor() {
const msg = `Attempted to set invalid field value`;
super(msg);
}
}
export class MultiSelectValueError extends Error {
constructor() {
const msg = `Attempted to select multiple values for single-select field`;
super(msg);
}
}
export class MissingDAEntryError extends Error {
constructor(fieldName: string) {
const msg = `No /DA (default appearance) entry found for field: ${fieldName}`;
super(msg);
}
}
export class MissingTfOperatorError extends Error {
constructor(fieldName: string) {
const msg = `No Tf operator found for DA of field: ${fieldName}`;
super(msg);
}
}
/***** Parser Errors ******/
export interface Position {
line: number;
column: number;
offset: number;
}
export class NumberParsingError extends Error {
constructor(pos: Position, value: string) {
const msg =
`Failed to parse number ` +
`(line:${pos.line} col:${pos.column} offset=${pos.offset}): "${value}"`;
super(msg);
}
}
export class PDFParsingError extends Error {
constructor(pos: Position, details: string) {
const msg =
`Failed to parse PDF document ` +
`(line:${pos.line} col:${pos.column} offset=${pos.offset}): ${details}`;
super(msg);
}
}
export class NextByteAssertionError extends PDFParsingError {
constructor(pos: Position, expectedByte: number, actualByte: number) {
const msg = `Expected next byte to be ${expectedByte} but it was actually ${actualByte}`;
super(pos, msg);
}
}
export class PDFObjectParsingError extends PDFParsingError {
constructor(pos: Position, byte: number) {
const msg = `Failed to parse PDF object starting with the following byte: ${byte}`;
super(pos, msg);
}
}
export class PDFInvalidObjectParsingError extends PDFParsingError {
constructor(pos: Position) {
const msg = `Failed to parse invalid PDF object`;
super(pos, msg);
}
}
export class PDFStreamParsingError extends PDFParsingError {
constructor(pos: Position) {
const msg = `Failed to parse PDF stream`;
super(pos, msg);
}
}
export class UnbalancedParenthesisError extends PDFParsingError {
constructor(pos: Position) {
const msg = `Failed to parse PDF literal string due to unbalanced parenthesis`;
super(pos, msg);
}
}
export class StalledParserError extends PDFParsingError {
constructor(pos: Position) {
const msg = `Parser stalled`;
super(pos, msg);
}
}
export class MissingPDFHeaderError extends PDFParsingError {
constructor(pos: Position) {
const msg = `No PDF header found`;
super(pos, msg);
}
}
export class MissingKeywordError extends PDFParsingError {
constructor(pos: Position, keyword: number[]) {
const msg = `Did not find expected keyword '${arrayAsString(keyword)}'`;
super(pos, msg);
}
}

69
node_modules/pdf-lib/src/core/index.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
export * from 'src/core/errors';
export { default as CharCodes } from 'src/core/syntax/CharCodes';
export { default as PDFContext } from 'src/core/PDFContext';
export { default as PDFObjectCopier } from 'src/core/PDFObjectCopier';
export { default as PDFWriter } from 'src/core/writers/PDFWriter';
export { default as PDFStreamWriter } from 'src/core/writers/PDFStreamWriter';
export { default as PDFHeader } from 'src/core/document/PDFHeader';
export { default as PDFTrailer } from 'src/core/document/PDFTrailer';
export { default as PDFTrailerDict } from 'src/core/document/PDFTrailerDict';
export { default as PDFCrossRefSection } from 'src/core/document/PDFCrossRefSection';
export { default as StandardFontEmbedder } from 'src/core/embedders/StandardFontEmbedder';
export { default as CustomFontEmbedder } from 'src/core/embedders/CustomFontEmbedder';
export { default as CustomFontSubsetEmbedder } from 'src/core/embedders/CustomFontSubsetEmbedder';
export {
default as FileEmbedder,
AFRelationship,
} from 'src/core/embedders/FileEmbedder';
export { default as JpegEmbedder } from 'src/core/embedders/JpegEmbedder';
export { default as PngEmbedder } from 'src/core/embedders/PngEmbedder';
export {
default as PDFPageEmbedder,
PageBoundingBox,
} from 'src/core/embedders/PDFPageEmbedder';
export {
default as ViewerPreferences,
NonFullScreenPageMode,
ReadingDirection,
PrintScaling,
Duplex,
} from 'src/core/interactive/ViewerPreferences';
export { default as PDFObject } from 'src/core/objects/PDFObject';
export { default as PDFBool } from 'src/core/objects/PDFBool';
export { default as PDFNumber } from 'src/core/objects/PDFNumber';
export { default as PDFString } from 'src/core/objects/PDFString';
export { default as PDFHexString } from 'src/core/objects/PDFHexString';
export { default as PDFName } from 'src/core/objects/PDFName';
export { default as PDFNull } from 'src/core/objects/PDFNull';
export { default as PDFArray } from 'src/core/objects/PDFArray';
export { default as PDFDict } from 'src/core/objects/PDFDict';
export { default as PDFRef } from 'src/core/objects/PDFRef';
export { default as PDFInvalidObject } from 'src/core/objects/PDFInvalidObject';
export { default as PDFStream } from 'src/core/objects/PDFStream';
export { default as PDFRawStream } from 'src/core/objects/PDFRawStream';
export { default as PDFCatalog } from 'src/core/structures/PDFCatalog';
export { default as PDFContentStream } from 'src/core/structures/PDFContentStream';
export { default as PDFCrossRefStream } from 'src/core/structures/PDFCrossRefStream';
export { default as PDFObjectStream } from 'src/core/structures/PDFObjectStream';
export { default as PDFPageTree } from 'src/core/structures/PDFPageTree';
export { default as PDFPageLeaf } from 'src/core/structures/PDFPageLeaf';
export { default as PDFFlateStream } from 'src/core/structures/PDFFlateStream';
export { default as PDFOperator } from 'src/core/operators/PDFOperator';
export { default as PDFOperatorNames } from 'src/core/operators/PDFOperatorNames';
export { default as PDFObjectParser } from 'src/core/parser/PDFObjectParser';
export { default as PDFObjectStreamParser } from 'src/core/parser/PDFObjectStreamParser';
export { default as PDFParser } from 'src/core/parser/PDFParser';
export { default as PDFXRefStreamParser } from 'src/core/parser/PDFXRefStreamParser';
export { decodePDFRawStream } from 'src/core/streams/decode';
export * from 'src/core/annotation';
export * from 'src/core/acroform';

View File

@@ -0,0 +1,579 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFBool from 'src/core/objects/PDFBool';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFContext from 'src/core/PDFContext';
import {
assertEachIs,
assertInteger,
assertIsOneOf,
assertRange,
} from 'src/utils';
const asEnum = <T extends string | number, U extends { [key: string]: T }>(
rawValue: T | undefined,
enumType: U,
): U[keyof U] | undefined => {
if (rawValue === undefined) return undefined;
return enumType[rawValue];
};
export enum NonFullScreenPageMode {
/**
* After exiting FullScreen mode, neither the document outline nor thumbnail
* images should be visible.
*/
UseNone = 'UseNone',
/** After exiting FullScreen mode, the document outline should be visible. */
UseOutlines = 'UseOutlines',
/** After exiting FullScreen mode, thumbnail images should be visible. */
UseThumbs = 'UseThumbs',
/**
* After exiting FullScreen mode, the optional content group panel should be
* visible.
*/
UseOC = 'UseOC',
}
export enum ReadingDirection {
/** The predominant reading order is Left to Right. */
L2R = 'L2R',
/**
* The predominant reading order is Right to left (including vertical writing
* systems, such as Chinese, Japanese and Korean).
*/
R2L = 'R2L',
}
export enum PrintScaling {
/** No page scaling. */
None = 'None',
/* Use the PDF reader's default print scaling. */
AppDefault = 'AppDefault',
}
export enum Duplex {
/** The PDF reader should print single-sided. */
Simplex = 'Simplex',
/**
* The PDF reader should print double sided and flip on the short edge of the
* sheet.
*/
DuplexFlipShortEdge = 'DuplexFlipShortEdge',
/**
* The PDF reader should print double sided and flip on the long edge of the
* sheet.
*/
DuplexFlipLongEdge = 'DuplexFlipLongEdge',
}
type BoolViewerPrefKey =
| 'HideToolbar'
| 'HideMenubar'
| 'HideWindowUI'
| 'FitWindow'
| 'CenterWindow'
| 'DisplayDocTitle'
| 'PickTrayByPDFSize';
type NameViewerPrefKey =
| 'NonFullScreenPageMode'
| 'Direction'
| 'PrintScaling'
| 'Duplex';
interface PageRange {
start: number;
end: number;
}
class ViewerPreferences {
/** @ignore */
readonly dict: PDFDict;
/** @ignore */
static fromDict = (dict: PDFDict): ViewerPreferences =>
new ViewerPreferences(dict);
/** @ignore */
static create = (context: PDFContext) => {
const dict = context.obj({});
return new ViewerPreferences(dict);
};
/** @ignore */
protected constructor(dict: PDFDict) {
this.dict = dict;
}
protected lookupBool(key: BoolViewerPrefKey): PDFBool | undefined {
const returnObj = this.dict.lookup(PDFName.of(key));
if (returnObj instanceof PDFBool) return returnObj;
return undefined;
}
protected lookupName(key: NameViewerPrefKey): PDFName | undefined {
const returnObj = this.dict.lookup(PDFName.of(key));
if (returnObj instanceof PDFName) return returnObj;
return undefined;
}
/** @ignore */
HideToolbar(): PDFBool | undefined {
return this.lookupBool('HideToolbar');
}
/** @ignore */
HideMenubar(): PDFBool | undefined {
return this.lookupBool('HideMenubar');
}
/** @ignore */
HideWindowUI(): PDFBool | undefined {
return this.lookupBool('HideWindowUI');
}
/** @ignore */
FitWindow(): PDFBool | undefined {
return this.lookupBool('FitWindow');
}
/** @ignore */
CenterWindow(): PDFBool | undefined {
return this.lookupBool('CenterWindow');
}
/** @ignore */
DisplayDocTitle(): PDFBool | undefined {
return this.lookupBool('DisplayDocTitle');
}
/** @ignore */
NonFullScreenPageMode(): PDFName | undefined {
return this.lookupName('NonFullScreenPageMode');
}
/** @ignore */
Direction(): PDFName | undefined {
return this.lookupName('Direction');
}
/** @ignore */
PrintScaling(): PDFName | undefined {
return this.lookupName('PrintScaling');
}
/** @ignore */
Duplex(): PDFName | undefined {
return this.lookupName('Duplex');
}
/** @ignore */
PickTrayByPDFSize(): PDFBool | undefined {
return this.lookupBool('PickTrayByPDFSize');
}
/** @ignore */
PrintPageRange(): PDFArray | undefined {
const PrintPageRange = this.dict.lookup(PDFName.of('PrintPageRange'));
if (PrintPageRange instanceof PDFArray) return PrintPageRange;
return undefined;
}
/** @ignore */
NumCopies(): PDFNumber | undefined {
const NumCopies = this.dict.lookup(PDFName.of('NumCopies'));
if (NumCopies instanceof PDFNumber) return NumCopies;
return undefined;
}
/**
* Returns `true` if PDF readers should hide the toolbar menus when displaying
* this document.
* @returns Whether or not toolbars should be hidden.
*/
getHideToolbar(): boolean {
return this.HideToolbar()?.asBoolean() ?? false;
}
/**
* Returns `true` if PDF readers should hide the menu bar when displaying this
* document.
* @returns Whether or not the menu bar should be hidden.
*/
getHideMenubar(): boolean {
return this.HideMenubar()?.asBoolean() ?? false;
}
/**
* Returns `true` if PDF readers should hide the user interface elements in
* the document's window (such as scroll bars and navigation controls),
* leaving only the document's contents displayed.
* @returns Whether or not user interface elements should be hidden.
*/
getHideWindowUI(): boolean {
return this.HideWindowUI()?.asBoolean() ?? false;
}
/**
* Returns `true` if PDF readers should resize the document's window to fit
* the size of the first displayed page.
* @returns Whether or not the window should be resized to fit.
*/
getFitWindow(): boolean {
return this.FitWindow()?.asBoolean() ?? false;
}
/**
* Returns `true` if PDF readers should position the document's window in the
* center of the screen.
* @returns Whether or not to center the document window.
*/
getCenterWindow(): boolean {
return this.CenterWindow()?.asBoolean() ?? false;
}
/**
* Returns `true` if the window's title bar should display the document
* `Title`, taken from the document metadata (see [[PDFDocument.getTitle]]).
* Returns `false` if the title bar should instead display the filename of the
* PDF file.
* @returns Whether to display the document title.
*/
getDisplayDocTitle(): boolean {
return this.DisplayDocTitle()?.asBoolean() ?? false;
}
/**
* Returns the page mode, which tells the PDF reader how to display the
* document after exiting full-screen mode.
* @returns The page mode after exiting full-screen mode.
*/
getNonFullScreenPageMode(): NonFullScreenPageMode {
const mode = this.NonFullScreenPageMode()?.decodeText();
return asEnum(mode, NonFullScreenPageMode) ?? NonFullScreenPageMode.UseNone;
}
/**
* Returns the predominant reading order for text.
* @returns The text reading order.
*/
getReadingDirection(): ReadingDirection {
const direction = this.Direction()?.decodeText();
return asEnum(direction, ReadingDirection) ?? ReadingDirection.L2R;
}
/**
* Returns the page scaling option that the PDF reader should select when the
* print dialog is displayed.
* @returns The page scaling option.
*/
getPrintScaling(): PrintScaling {
const scaling = this.PrintScaling()?.decodeText();
return asEnum(scaling, PrintScaling) ?? PrintScaling.AppDefault;
}
/**
* Returns the paper handling option that should be used when printing the
* file from the print dialog.
* @returns The paper handling option.
*/
getDuplex(): Duplex | undefined {
const duplex = this.Duplex()?.decodeText();
return asEnum(duplex, Duplex);
}
/**
* Returns `true` if the PDF page size should be used to select the input
* paper tray.
* @returns Whether or not the PDF page size should be used to select the
* input paper tray.
*/
getPickTrayByPDFSize(): boolean | undefined {
return this.PickTrayByPDFSize()?.asBoolean();
}
/**
* Returns an array of page number ranges, which are the values used to
* initialize the print dialog box when the file is printed. Each range
* specifies the first (`start`) and last (`end`) pages in a sub-range of
* pages to be printed. The first page of the PDF file is denoted by 0.
* For example:
* ```js
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* const includesPage3 = viewerPrefs
* .getPrintRanges()
* .some(pr => pr.start =< 2 && pr.end >= 2)
* if (includesPage3) console.log('printRange includes page 3')
* ```
* @returns An array of objects, each with the properties `start` and `end`,
* denoting page indices. If not, specified an empty array is
* returned.
*/
getPrintPageRange(): PageRange[] {
const rng = this.PrintPageRange();
if (!rng) return [];
const pageRanges: PageRange[] = [];
for (let i = 0; i < rng.size(); i += 2) {
// Despite the spec clearly stating that "The first page of the PDF file
// shall be donoted by 1", several test PDFs (spec 1.7) created in
// Acrobat XI 11.0 and also read with Reader DC 2020.013 indicate this is
// actually a 0 based index.
const start = rng.lookup(i, PDFNumber).asNumber();
const end = rng.lookup(i + 1, PDFNumber).asNumber();
pageRanges.push({ start, end });
}
return pageRanges;
}
/**
* Returns the number of copies to be printed when the print dialog is opened
* for this document.
* @returns The default number of copies to be printed.
*/
getNumCopies(): number {
return this.NumCopies()?.asNumber() ?? 1;
}
/**
* Choose whether the PDF reader's toolbars should be hidden while the
* document is active.
* @param hideToolbar `true` if the toolbar should be hidden.
*/
setHideToolbar(hideToolbar: boolean) {
const HideToolbar = this.dict.context.obj(hideToolbar);
this.dict.set(PDFName.of('HideToolbar'), HideToolbar);
}
/**
* Choose whether the PDF reader's menu bar should be hidden while the
* document is active.
* @param hideMenubar `true` if the menu bar should be hidden.
*/
setHideMenubar(hideMenubar: boolean) {
const HideMenubar = this.dict.context.obj(hideMenubar);
this.dict.set(PDFName.of('HideMenubar'), HideMenubar);
}
/**
* Choose whether the PDF reader should hide user interface elements in the
* document's window (such as scroll bars and navigation controls), leaving
* only the document's contents displayed.
* @param hideWindowUI `true` if the user interface elements should be hidden.
*/
setHideWindowUI(hideWindowUI: boolean) {
const HideWindowUI = this.dict.context.obj(hideWindowUI);
this.dict.set(PDFName.of('HideWindowUI'), HideWindowUI);
}
/**
* Choose whether the PDF reader should resize the document's window to fit
* the size of the first displayed page.
* @param fitWindow `true` if the window should be resized.
*/
setFitWindow(fitWindow: boolean) {
const FitWindow = this.dict.context.obj(fitWindow);
this.dict.set(PDFName.of('FitWindow'), FitWindow);
}
/**
* Choose whether the PDF reader should position the document's window in the
* center of the screen.
* @param centerWindow `true` if the window should be centered.
*/
setCenterWindow(centerWindow: boolean) {
const CenterWindow = this.dict.context.obj(centerWindow);
this.dict.set(PDFName.of('CenterWindow'), CenterWindow);
}
/**
* Choose whether the window's title bar should display the document `Title`
* taken from the document metadata (see [[PDFDocument.setTitle]]). If
* `false`, the title bar should instead display the PDF filename.
* @param displayTitle `true` if the document title should be displayed.
*/
setDisplayDocTitle(displayTitle: boolean) {
const DisplayDocTitle = this.dict.context.obj(displayTitle);
this.dict.set(PDFName.of('DisplayDocTitle'), DisplayDocTitle);
}
/**
* Choose how the PDF reader should display the document upon exiting
* full-screen mode. This entry is meaningful only if the value of the
* `PageMode` entry in the document's [[PDFCatalog]] is `FullScreen`.
*
* For example:
* ```js
* import { PDFDocument, NonFullScreenPageMode, PDFName } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
*
* // Set the PageMode
* pdfDoc.catalog.set(PDFName.of('PageMode'),PDFName.of('FullScreen'))
*
* // Set what happens when full-screen is closed
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* viewerPrefs.setNonFullScreenPageMode(NonFullScreenPageMode.UseOutlines)
* ```
*
* @param nonFullScreenPageMode How the document should be displayed upon
* exiting full screen mode.
*/
setNonFullScreenPageMode(nonFullScreenPageMode: NonFullScreenPageMode) {
assertIsOneOf(
nonFullScreenPageMode,
'nonFullScreenPageMode',
NonFullScreenPageMode,
);
const mode = PDFName.of(nonFullScreenPageMode);
this.dict.set(PDFName.of('NonFullScreenPageMode'), mode);
}
/**
* Choose the predominant reading order for text.
*
* This entry has no direct effect on the document's contents or page
* numbering, but may be used to determine the relative positioning of pages
* when displayed side by side or printed n-up.
*
* For example:
* ```js
* import { PDFDocument, ReadingDirection } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* viewerPrefs.setReadingDirection(ReadingDirection.R2L)
* ```
*
* @param readingDirection The reading order for text.
*/
setReadingDirection(readingDirection: ReadingDirection) {
assertIsOneOf(readingDirection, 'readingDirection', ReadingDirection);
const direction = PDFName.of(readingDirection);
this.dict.set(PDFName.of('Direction'), direction);
}
/**
* Choose the page scaling option that should be selected when a print dialog
* is displayed for this document.
*
* For example:
* ```js
* import { PDFDocument, PrintScaling } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* viewerPrefs.setPrintScaling(PrintScaling.None)
* ```
*
* @param printScaling The print scaling option.
*/
setPrintScaling(printScaling: PrintScaling) {
assertIsOneOf(printScaling, 'printScaling', PrintScaling);
const scaling = PDFName.of(printScaling);
this.dict.set(PDFName.of('PrintScaling'), scaling);
}
/**
* Choose the paper handling option that should be selected by default in the
* print dialog.
*
* For example:
* ```js
* import { PDFDocument, Duplex } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
* viewerPrefs.setDuplex(Duplex.DuplexFlipShortEdge)
* ```
*
* @param duplex The double or single sided printing option.
*/
setDuplex(duplex: Duplex) {
assertIsOneOf(duplex, 'duplex', Duplex);
const dup = PDFName.of(duplex);
this.dict.set(PDFName.of('Duplex'), dup);
}
/**
* Choose whether the PDF document's page size should be used to select the
* input paper tray when printing. This setting influences only the preset
* values used to populate the print dialog presented by a PDF reader.
*
* If PickTrayByPDFSize is true, the check box in the print dialog associated
* with input paper tray should be checked. This setting has no effect on
* operating systems that do not provide the ability to pick the input tray
* by size.
*
* @param pickTrayByPDFSize `true` if the document's page size should be used
* to select the input paper tray.
*/
setPickTrayByPDFSize(pickTrayByPDFSize: boolean) {
const PickTrayByPDFSize = this.dict.context.obj(pickTrayByPDFSize);
this.dict.set(PDFName.of('PickTrayByPDFSize'), PickTrayByPDFSize);
}
/**
* Choose the page numbers used to initialize the print dialog box when the
* file is printed. The first page of the PDF file is denoted by 0.
*
* For example:
* ```js
* import { PDFDocument } from 'pdf-lib'
*
* const pdfDoc = await PDFDocument.create()
* const viewerPrefs = pdfDoc.catalog.getOrCreateViewerPreferences()
*
* // We can set the default print range to only the first page
* viewerPrefs.setPrintPageRange({ start: 0, end: 0 })
*
* // Or we can supply noncontiguous ranges (e.g. pages 1, 3, and 5-7)
* viewerPrefs.setPrintPageRange([
* { start: 0, end: 0 },
* { start: 2, end: 2 },
* { start: 4, end: 6 },
* ])
* ```
*
* @param printPageRange An object or array of objects, each with the
* properties `start` and `end`, denoting a range of
* page indices.
*/
setPrintPageRange(printPageRange: PageRange[] | PageRange) {
if (!Array.isArray(printPageRange)) printPageRange = [printPageRange];
const flatRange: number[] = [];
for (let idx = 0, len = printPageRange.length; idx < len; idx++) {
flatRange.push(printPageRange[idx].start);
flatRange.push(printPageRange[idx].end);
}
assertEachIs(flatRange, 'printPageRange', ['number']);
const pageRanges = this.dict.context.obj(flatRange);
this.dict.set(PDFName.of('PrintPageRange'), pageRanges);
}
/**
* Choose the default number of copies to be printed when the print dialog is
* opened for this file.
* @param numCopies The default number of copies.
*/
setNumCopies(numCopies: number) {
assertRange(numCopies, 'numCopies', 1, Number.MAX_VALUE);
assertInteger(numCopies, 'numCopies');
const NumCopies = this.dict.context.obj(numCopies);
this.dict.set(PDFName.of('NumCopies'), NumCopies);
}
}
export default ViewerPreferences;

185
node_modules/pdf-lib/src/core/objects/PDFArray.ts generated vendored Normal file
View File

@@ -0,0 +1,185 @@
import PDFBool from 'src/core/objects/PDFBool';
import PDFDict from 'src/core/objects/PDFDict';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFString from 'src/core/objects/PDFString';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
import { PDFArrayIsNotRectangleError } from 'src/core/errors';
import PDFRawStream from 'src/core/objects/PDFRawStream';
class PDFArray extends PDFObject {
static withContext = (context: PDFContext) => new PDFArray(context);
private readonly array: PDFObject[];
private readonly context: PDFContext;
private constructor(context: PDFContext) {
super();
this.array = [];
this.context = context;
}
size(): number {
return this.array.length;
}
push(object: PDFObject): void {
this.array.push(object);
}
insert(index: number, object: PDFObject): void {
this.array.splice(index, 0, object);
}
indexOf(object: PDFObject): number | undefined {
const index = this.array.indexOf(object);
return index === -1 ? undefined : index;
}
remove(index: number): void {
this.array.splice(index, 1);
}
set(idx: number, object: PDFObject): void {
this.array[idx] = object;
}
get(index: number): PDFObject {
return this.array[index];
}
lookupMaybe(index: number, type: typeof PDFArray): PDFArray | undefined;
lookupMaybe(index: number, type: typeof PDFBool): PDFBool | undefined;
lookupMaybe(index: number, type: typeof PDFDict): PDFDict | undefined;
lookupMaybe(
index: number,
type: typeof PDFHexString,
): PDFHexString | undefined;
lookupMaybe(index: number, type: typeof PDFName): PDFName | undefined;
lookupMaybe(index: number, type: typeof PDFNull): typeof PDFNull | undefined;
lookupMaybe(index: number, type: typeof PDFNumber): PDFNumber | undefined;
lookupMaybe(index: number, type: typeof PDFStream): PDFStream | undefined;
lookupMaybe(
index: number,
type: typeof PDFRawStream,
): PDFRawStream | undefined;
lookupMaybe(index: number, type: typeof PDFRef): PDFRef | undefined;
lookupMaybe(index: number, type: typeof PDFString): PDFString | undefined;
lookupMaybe(
index: number,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString | undefined;
lookupMaybe(index: number, ...types: any[]) {
return this.context.lookupMaybe(
this.get(index),
// @ts-ignore
...types,
) as any;
}
lookup(index: number): PDFObject | undefined;
lookup(index: number, type: typeof PDFArray): PDFArray;
lookup(index: number, type: typeof PDFBool): PDFBool;
lookup(index: number, type: typeof PDFDict): PDFDict;
lookup(index: number, type: typeof PDFHexString): PDFHexString;
lookup(index: number, type: typeof PDFName): PDFName;
lookup(index: number, type: typeof PDFNull): typeof PDFNull;
lookup(index: number, type: typeof PDFNumber): PDFNumber;
lookup(index: number, type: typeof PDFStream): PDFStream;
lookup(index: number, type: typeof PDFRawStream): PDFRawStream;
lookup(index: number, type: typeof PDFRef): PDFRef;
lookup(index: number, type: typeof PDFString): PDFString;
lookup(
index: number,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString;
lookup(index: number, ...types: any[]) {
return this.context.lookup(
this.get(index),
// @ts-ignore
...types,
) as any;
}
asRectangle(): { x: number; y: number; width: number; height: number } {
if (this.size() !== 4) throw new PDFArrayIsNotRectangleError(this.size());
const lowerLeftX = this.lookup(0, PDFNumber).asNumber();
const lowerLeftY = this.lookup(1, PDFNumber).asNumber();
const upperRightX = this.lookup(2, PDFNumber).asNumber();
const upperRightY = this.lookup(3, PDFNumber).asNumber();
const x = lowerLeftX;
const y = lowerLeftY;
const width = upperRightX - lowerLeftX;
const height = upperRightY - lowerLeftY;
return { x, y, width, height };
}
asArray(): PDFObject[] {
return this.array.slice();
}
clone(context?: PDFContext): PDFArray {
const clone = PDFArray.withContext(context || this.context);
for (let idx = 0, len = this.size(); idx < len; idx++) {
clone.push(this.array[idx]);
}
return clone;
}
toString(): string {
let arrayString = '[ ';
for (let idx = 0, len = this.size(); idx < len; idx++) {
arrayString += this.get(idx).toString();
arrayString += ' ';
}
arrayString += ']';
return arrayString;
}
sizeInBytes(): number {
let size = 3;
for (let idx = 0, len = this.size(); idx < len; idx++) {
size += this.get(idx).sizeInBytes() + 1;
}
return size;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.LeftSquareBracket;
buffer[offset++] = CharCodes.Space;
for (let idx = 0, len = this.size(); idx < len; idx++) {
offset += this.get(idx).copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Space;
}
buffer[offset++] = CharCodes.RightSquareBracket;
return offset - initialOffset;
}
scalePDFNumbers(x: number, y: number): void {
for (let idx = 0, len = this.size(); idx < len; idx++) {
const el = this.lookup(idx);
if (el instanceof PDFNumber) {
const factor = idx % 2 === 0 ? x : y;
this.set(idx, PDFNumber.of(el.asNumber() * factor));
}
}
}
}
export default PDFArray;

53
node_modules/pdf-lib/src/core/objects/PDFBool.ts generated vendored Normal file
View File

@@ -0,0 +1,53 @@
import { PrivateConstructorError } from 'src/core/errors';
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
const ENFORCER = {};
class PDFBool extends PDFObject {
static readonly True = new PDFBool(ENFORCER, true);
static readonly False = new PDFBool(ENFORCER, false);
private readonly value: boolean;
private constructor(enforcer: any, value: boolean) {
if (enforcer !== ENFORCER) throw new PrivateConstructorError('PDFBool');
super();
this.value = value;
}
asBoolean(): boolean {
return this.value;
}
clone(): PDFBool {
return this;
}
toString(): string {
return String(this.value);
}
sizeInBytes(): number {
return this.value ? 4 : 5;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
if (this.value) {
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.u;
buffer[offset++] = CharCodes.e;
return 4;
} else {
buffer[offset++] = CharCodes.f;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.l;
buffer[offset++] = CharCodes.s;
buffer[offset++] = CharCodes.e;
return 5;
}
}
}
export default PDFBool;

226
node_modules/pdf-lib/src/core/objects/PDFDict.ts generated vendored Normal file
View File

@@ -0,0 +1,226 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFBool from 'src/core/objects/PDFBool';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFString from 'src/core/objects/PDFString';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
export type DictMap = Map<PDFName, PDFObject>;
class PDFDict extends PDFObject {
static withContext = (context: PDFContext) => new PDFDict(new Map(), context);
static fromMapWithContext = (map: DictMap, context: PDFContext) =>
new PDFDict(map, context);
readonly context: PDFContext;
private readonly dict: DictMap;
protected constructor(map: DictMap, context: PDFContext) {
super();
this.dict = map;
this.context = context;
}
keys(): PDFName[] {
return Array.from(this.dict.keys());
}
values(): PDFObject[] {
return Array.from(this.dict.values());
}
entries(): [PDFName, PDFObject][] {
return Array.from(this.dict.entries());
}
set(key: PDFName, value: PDFObject): void {
this.dict.set(key, value);
}
get(
key: PDFName,
// TODO: `preservePDFNull` is for backwards compatibility. Should be
// removed in next breaking API change.
preservePDFNull = false,
): PDFObject | undefined {
const value = this.dict.get(key);
if (value === PDFNull && !preservePDFNull) return undefined;
return value;
}
has(key: PDFName): boolean {
const value = this.dict.get(key);
return value !== undefined && value !== PDFNull;
}
lookupMaybe(key: PDFName, type: typeof PDFArray): PDFArray | undefined;
lookupMaybe(key: PDFName, type: typeof PDFBool): PDFBool | undefined;
lookupMaybe(key: PDFName, type: typeof PDFDict): PDFDict | undefined;
lookupMaybe(
key: PDFName,
type: typeof PDFHexString,
): PDFHexString | undefined;
lookupMaybe(key: PDFName, type: typeof PDFName): PDFName | undefined;
lookupMaybe(key: PDFName, type: typeof PDFNull): typeof PDFNull | undefined;
lookupMaybe(key: PDFName, type: typeof PDFNumber): PDFNumber | undefined;
lookupMaybe(key: PDFName, type: typeof PDFStream): PDFStream | undefined;
lookupMaybe(key: PDFName, type: typeof PDFRef): PDFRef | undefined;
lookupMaybe(key: PDFName, type: typeof PDFString): PDFString | undefined;
lookupMaybe(
ref: PDFName,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString | undefined;
lookupMaybe(
ref: PDFName,
type1: typeof PDFDict,
type2: typeof PDFStream,
): PDFDict | PDFStream | undefined;
lookupMaybe(
ref: PDFName,
type1: typeof PDFString,
type2: typeof PDFHexString,
type3: typeof PDFArray,
): PDFString | PDFHexString | PDFArray | undefined;
lookupMaybe(key: PDFName, ...types: any[]) {
// TODO: `preservePDFNull` is for backwards compatibility. Should be
// removed in next breaking API change.
const preservePDFNull = types.includes(PDFNull);
const value = this.context.lookupMaybe(
this.get(key, preservePDFNull),
// @ts-ignore
...types,
) as any;
if (value === PDFNull && !preservePDFNull) return undefined;
return value;
}
lookup(key: PDFName): PDFObject | undefined;
lookup(key: PDFName, type: typeof PDFArray): PDFArray;
lookup(key: PDFName, type: typeof PDFBool): PDFBool;
lookup(key: PDFName, type: typeof PDFDict): PDFDict;
lookup(key: PDFName, type: typeof PDFHexString): PDFHexString;
lookup(key: PDFName, type: typeof PDFName): PDFName;
lookup(key: PDFName, type: typeof PDFNull): typeof PDFNull;
lookup(key: PDFName, type: typeof PDFNumber): PDFNumber;
lookup(key: PDFName, type: typeof PDFStream): PDFStream;
lookup(key: PDFName, type: typeof PDFRef): PDFRef;
lookup(key: PDFName, type: typeof PDFString): PDFString;
lookup(
ref: PDFName,
type1: typeof PDFString,
type2: typeof PDFHexString,
): PDFString | PDFHexString;
lookup(
ref: PDFName,
type1: typeof PDFDict,
type2: typeof PDFStream,
): PDFDict | PDFStream;
lookup(
ref: PDFName,
type1: typeof PDFString,
type2: typeof PDFHexString,
type3: typeof PDFArray,
): PDFString | PDFHexString | PDFArray;
lookup(key: PDFName, ...types: any[]) {
// TODO: `preservePDFNull` is for backwards compatibility. Should be
// removed in next breaking API change.
const preservePDFNull = types.includes(PDFNull);
const value = this.context.lookup(
this.get(key, preservePDFNull),
// @ts-ignore
...types,
) as any;
if (value === PDFNull && !preservePDFNull) return undefined;
return value;
}
delete(key: PDFName): boolean {
return this.dict.delete(key);
}
asMap(): Map<PDFName, PDFObject> {
return new Map(this.dict);
}
/** Generate a random key that doesn't exist in current key set */
uniqueKey(tag = ''): PDFName {
const existingKeys = this.keys();
let key = PDFName.of(this.context.addRandomSuffix(tag, 10));
while (existingKeys.includes(key)) {
key = PDFName.of(this.context.addRandomSuffix(tag, 10));
}
return key;
}
clone(context?: PDFContext): PDFDict {
const clone = PDFDict.withContext(context || this.context);
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
clone.set(key, value);
}
return clone;
}
toString(): string {
let dictString = '<<\n';
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
dictString += key.toString() + ' ' + value.toString() + '\n';
}
dictString += '>>';
return dictString;
}
sizeInBytes(): number {
let size = 5;
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
size += key.sizeInBytes() + value.sizeInBytes() + 2;
}
return size;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
buffer[offset++] = CharCodes.LessThan;
buffer[offset++] = CharCodes.LessThan;
buffer[offset++] = CharCodes.Newline;
const entries = this.entries();
for (let idx = 0, len = entries.length; idx < len; idx++) {
const [key, value] = entries[idx];
offset += key.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Space;
offset += value.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
}
buffer[offset++] = CharCodes.GreaterThan;
buffer[offset++] = CharCodes.GreaterThan;
return offset - initialOffset;
}
}
export default PDFDict;

94
node_modules/pdf-lib/src/core/objects/PDFHexString.ts generated vendored Normal file
View File

@@ -0,0 +1,94 @@
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
import {
copyStringIntoBuffer,
toHexStringOfMinLength,
utf16Decode,
utf16Encode,
pdfDocEncodingDecode,
parseDate,
hasUtf16BOM,
} from 'src/utils';
import { InvalidPDFDateStringError } from 'src/core/errors';
class PDFHexString extends PDFObject {
static of = (value: string) => new PDFHexString(value);
static fromText = (value: string) => {
const encoded = utf16Encode(value);
let hex = '';
for (let idx = 0, len = encoded.length; idx < len; idx++) {
hex += toHexStringOfMinLength(encoded[idx], 4);
}
return new PDFHexString(hex);
};
private readonly value: string;
constructor(value: string) {
super();
this.value = value;
}
asBytes(): Uint8Array {
// Append a zero if the number of digits is odd. See PDF spec 7.3.4.3
const hex = this.value + (this.value.length % 2 === 1 ? '0' : '');
const hexLength = hex.length;
const bytes = new Uint8Array(hex.length / 2);
let hexOffset = 0;
let bytesOffset = 0;
// Interpret each pair of hex digits as a single byte
while (hexOffset < hexLength) {
const byte = parseInt(hex.substring(hexOffset, hexOffset + 2), 16);
bytes[bytesOffset] = byte;
hexOffset += 2;
bytesOffset += 1;
}
return bytes;
}
decodeText(): string {
const bytes = this.asBytes();
if (hasUtf16BOM(bytes)) return utf16Decode(bytes);
return pdfDocEncodingDecode(bytes);
}
decodeDate(): Date {
const text = this.decodeText();
const date = parseDate(text);
if (!date) throw new InvalidPDFDateStringError(text);
return date;
}
asString(): string {
return this.value;
}
clone(): PDFHexString {
return PDFHexString.of(this.value);
}
toString(): string {
return `<${this.value}>`;
}
sizeInBytes(): number {
return this.value.length + 2;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
buffer[offset++] = CharCodes.LessThan;
offset += copyStringIntoBuffer(this.value, buffer, offset);
buffer[offset++] = CharCodes.GreaterThan;
return this.value.length + 2;
}
}
export default PDFHexString;

View File

@@ -0,0 +1,34 @@
import PDFObject from 'src/core/objects/PDFObject';
class PDFInvalidObject extends PDFObject {
static of = (data: Uint8Array) => new PDFInvalidObject(data);
private readonly data: Uint8Array;
private constructor(data: Uint8Array) {
super();
this.data = data;
}
clone(): PDFInvalidObject {
return PDFInvalidObject.of(this.data.slice());
}
toString(): string {
return `PDFInvalidObject(${this.data.length} bytes)`;
}
sizeInBytes(): number {
return this.data.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const length = this.data.length;
for (let idx = 0; idx < length; idx++) {
buffer[offset++] = this.data[idx];
}
return length;
}
}
export default PDFInvalidObject;

159
node_modules/pdf-lib/src/core/objects/PDFName.ts generated vendored Normal file
View File

@@ -0,0 +1,159 @@
import { PrivateConstructorError } from 'src/core/errors';
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
import { IsIrregular } from 'src/core/syntax/Irregular';
import {
charFromHexCode,
copyStringIntoBuffer,
toCharCode,
toHexString,
} from 'src/utils';
const decodeName = (name: string) =>
name.replace(/#([\dABCDEF]{2})/g, (_, hex) => charFromHexCode(hex));
const isRegularChar = (charCode: number) =>
charCode >= CharCodes.ExclamationPoint &&
charCode <= CharCodes.Tilde &&
!IsIrregular[charCode];
const ENFORCER = {};
const pool = new Map<string, PDFName>();
class PDFName extends PDFObject {
static of = (name: string): PDFName => {
const decodedValue = decodeName(name);
let instance = pool.get(decodedValue);
if (!instance) {
instance = new PDFName(ENFORCER, decodedValue);
pool.set(decodedValue, instance);
}
return instance;
};
/* tslint:disable member-ordering */
static readonly Length = PDFName.of('Length');
static readonly FlateDecode = PDFName.of('FlateDecode');
static readonly Resources = PDFName.of('Resources');
static readonly Font = PDFName.of('Font');
static readonly XObject = PDFName.of('XObject');
static readonly ExtGState = PDFName.of('ExtGState');
static readonly Contents = PDFName.of('Contents');
static readonly Type = PDFName.of('Type');
static readonly Parent = PDFName.of('Parent');
static readonly MediaBox = PDFName.of('MediaBox');
static readonly Page = PDFName.of('Page');
static readonly Annots = PDFName.of('Annots');
static readonly TrimBox = PDFName.of('TrimBox');
static readonly ArtBox = PDFName.of('ArtBox');
static readonly BleedBox = PDFName.of('BleedBox');
static readonly CropBox = PDFName.of('CropBox');
static readonly Rotate = PDFName.of('Rotate');
static readonly Title = PDFName.of('Title');
static readonly Author = PDFName.of('Author');
static readonly Subject = PDFName.of('Subject');
static readonly Creator = PDFName.of('Creator');
static readonly Keywords = PDFName.of('Keywords');
static readonly Producer = PDFName.of('Producer');
static readonly CreationDate = PDFName.of('CreationDate');
static readonly ModDate = PDFName.of('ModDate');
/* tslint:enable member-ordering */
private readonly encodedName: string;
private constructor(enforcer: any, name: string) {
if (enforcer !== ENFORCER) throw new PrivateConstructorError('PDFName');
super();
let encodedName = '/';
for (let idx = 0, len = name.length; idx < len; idx++) {
const character = name[idx];
const code = toCharCode(character);
encodedName += isRegularChar(code) ? character : `#${toHexString(code)}`;
}
this.encodedName = encodedName;
}
asBytes(): Uint8Array {
const bytes: number[] = [];
let hex = '';
let escaped = false;
const pushByte = (byte?: number) => {
if (byte !== undefined) bytes.push(byte);
escaped = false;
};
for (let idx = 1, len = this.encodedName.length; idx < len; idx++) {
const char = this.encodedName[idx];
const byte = toCharCode(char);
const nextChar = this.encodedName[idx + 1];
if (!escaped) {
if (byte === CharCodes.Hash) escaped = true;
else pushByte(byte);
} else {
if (
(byte >= CharCodes.Zero && byte <= CharCodes.Nine) ||
(byte >= CharCodes.a && byte <= CharCodes.f) ||
(byte >= CharCodes.A && byte <= CharCodes.F)
) {
hex += char;
if (
hex.length === 2 ||
!(
(nextChar >= '0' && nextChar <= '9') ||
(nextChar >= 'a' && nextChar <= 'f') ||
(nextChar >= 'A' && nextChar <= 'F')
)
) {
pushByte(parseInt(hex, 16));
hex = '';
}
} else {
pushByte(byte);
}
}
}
return new Uint8Array(bytes);
}
// TODO: This should probably use `utf8Decode()`
// TODO: Polyfill Array.from?
decodeText(): string {
const bytes = this.asBytes();
return String.fromCharCode(...Array.from(bytes));
}
asString(): string {
return this.encodedName;
}
/** @deprecated in favor of [[PDFName.asString]] */
value(): string {
return this.encodedName;
}
clone(): PDFName {
return this;
}
toString(): string {
return this.encodedName;
}
sizeInBytes(): number {
return this.encodedName.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
offset += copyStringIntoBuffer(this.encodedName, buffer, offset);
return this.encodedName.length;
}
}
export default PDFName;

30
node_modules/pdf-lib/src/core/objects/PDFNull.ts generated vendored Normal file
View File

@@ -0,0 +1,30 @@
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
class PDFNull extends PDFObject {
asNull(): null {
return null;
}
clone(): PDFNull {
return this;
}
toString(): string {
return 'null';
}
sizeInBytes(): number {
return 4;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
buffer[offset++] = CharCodes.n;
buffer[offset++] = CharCodes.u;
buffer[offset++] = CharCodes.l;
buffer[offset++] = CharCodes.l;
return 4;
}
}
export default new PDFNull();

44
node_modules/pdf-lib/src/core/objects/PDFNumber.ts generated vendored Normal file
View File

@@ -0,0 +1,44 @@
import { copyStringIntoBuffer, numberToString } from 'src/utils/index';
import PDFObject from 'src/core/objects/PDFObject';
class PDFNumber extends PDFObject {
static of = (value: number) => new PDFNumber(value);
private readonly numberValue: number;
private readonly stringValue: string;
private constructor(value: number) {
super();
this.numberValue = value;
this.stringValue = numberToString(value);
}
asNumber(): number {
return this.numberValue;
}
/** @deprecated in favor of [[PDFNumber.asNumber]] */
value(): number {
return this.numberValue;
}
clone(): PDFNumber {
return PDFNumber.of(this.numberValue);
}
toString(): string {
return this.stringValue;
}
sizeInBytes(): number {
return this.stringValue.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
offset += copyStringIntoBuffer(this.stringValue, buffer, offset);
return this.stringValue.length;
}
}
export default PDFNumber;

22
node_modules/pdf-lib/src/core/objects/PDFObject.ts generated vendored Normal file
View File

@@ -0,0 +1,22 @@
import { MethodNotImplementedError } from 'src/core/errors';
import PDFContext from 'src/core/PDFContext';
class PDFObject {
clone(_context?: PDFContext): PDFObject {
throw new MethodNotImplementedError(this.constructor.name, 'clone');
}
toString(): string {
throw new MethodNotImplementedError(this.constructor.name, 'toString');
}
sizeInBytes(): number {
throw new MethodNotImplementedError(this.constructor.name, 'sizeInBytes');
}
copyBytesInto(_buffer: Uint8Array, _offset: number): number {
throw new MethodNotImplementedError(this.constructor.name, 'copyBytesInto');
}
}
export default PDFObject;

38
node_modules/pdf-lib/src/core/objects/PDFRawStream.ts generated vendored Normal file
View File

@@ -0,0 +1,38 @@
import PDFDict from 'src/core/objects/PDFDict';
import PDFStream from 'src/core/objects/PDFStream';
import PDFContext from 'src/core/PDFContext';
import { arrayAsString } from 'src/utils';
class PDFRawStream extends PDFStream {
static of = (dict: PDFDict, contents: Uint8Array) =>
new PDFRawStream(dict, contents);
readonly contents: Uint8Array;
private constructor(dict: PDFDict, contents: Uint8Array) {
super(dict);
this.contents = contents;
}
asUint8Array(): Uint8Array {
return this.contents.slice();
}
clone(context?: PDFContext): PDFRawStream {
return PDFRawStream.of(this.dict.clone(context), this.contents.slice());
}
getContentsString(): string {
return arrayAsString(this.contents);
}
getContents(): Uint8Array {
return this.contents;
}
getContentsSize(): number {
return this.contents.length;
}
}
export default PDFRawStream;

55
node_modules/pdf-lib/src/core/objects/PDFRef.ts generated vendored Normal file
View File

@@ -0,0 +1,55 @@
import { PrivateConstructorError } from 'src/core/errors';
import PDFObject from 'src/core/objects/PDFObject';
import { copyStringIntoBuffer } from 'src/utils';
const ENFORCER = {};
const pool = new Map<string, PDFRef>();
class PDFRef extends PDFObject {
static of = (objectNumber: number, generationNumber = 0) => {
const tag = `${objectNumber} ${generationNumber} R`;
let instance = pool.get(tag);
if (!instance) {
instance = new PDFRef(ENFORCER, objectNumber, generationNumber);
pool.set(tag, instance);
}
return instance;
};
readonly objectNumber: number;
readonly generationNumber: number;
readonly tag: string;
private constructor(
enforcer: any,
objectNumber: number,
generationNumber: number,
) {
if (enforcer !== ENFORCER) throw new PrivateConstructorError('PDFRef');
super();
this.objectNumber = objectNumber;
this.generationNumber = generationNumber;
this.tag = `${objectNumber} ${generationNumber} R`;
}
clone(): PDFRef {
return this;
}
toString(): string {
return this.tag;
}
sizeInBytes(): number {
return this.tag.length;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
offset += copyStringIntoBuffer(this.tag, buffer, offset);
return this.tag.length;
}
}
export default PDFRef;

93
node_modules/pdf-lib/src/core/objects/PDFStream.ts generated vendored Normal file
View File

@@ -0,0 +1,93 @@
import { MethodNotImplementedError } from 'src/core/errors';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
class PDFStream extends PDFObject {
readonly dict: PDFDict;
constructor(dict: PDFDict) {
super();
this.dict = dict;
}
clone(_context?: PDFContext): PDFStream {
throw new MethodNotImplementedError(this.constructor.name, 'clone');
}
getContentsString(): string {
throw new MethodNotImplementedError(
this.constructor.name,
'getContentsString',
);
}
getContents(): Uint8Array {
throw new MethodNotImplementedError(this.constructor.name, 'getContents');
}
getContentsSize(): number {
throw new MethodNotImplementedError(
this.constructor.name,
'getContentsSize',
);
}
updateDict(): void {
const contentsSize = this.getContentsSize();
this.dict.set(PDFName.Length, PDFNumber.of(contentsSize));
}
sizeInBytes(): number {
this.updateDict();
return this.dict.sizeInBytes() + this.getContentsSize() + 18;
}
toString(): string {
this.updateDict();
let streamString = this.dict.toString();
streamString += '\nstream\n';
streamString += this.getContentsString();
streamString += '\nendstream';
return streamString;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
this.updateDict();
const initialOffset = offset;
offset += this.dict.copyBytesInto(buffer, offset);
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.s;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.m;
buffer[offset++] = CharCodes.Newline;
const contents = this.getContents();
for (let idx = 0, len = contents.length; idx < len; idx++) {
buffer[offset++] = contents[idx];
}
buffer[offset++] = CharCodes.Newline;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.n;
buffer[offset++] = CharCodes.d;
buffer[offset++] = CharCodes.s;
buffer[offset++] = CharCodes.t;
buffer[offset++] = CharCodes.r;
buffer[offset++] = CharCodes.e;
buffer[offset++] = CharCodes.a;
buffer[offset++] = CharCodes.m;
return offset - initialOffset;
}
}
export default PDFStream;

118
node_modules/pdf-lib/src/core/objects/PDFString.ts generated vendored Normal file
View File

@@ -0,0 +1,118 @@
import PDFObject from 'src/core/objects/PDFObject';
import CharCodes from 'src/core/syntax/CharCodes';
import {
copyStringIntoBuffer,
padStart,
utf16Decode,
pdfDocEncodingDecode,
toCharCode,
parseDate,
hasUtf16BOM,
} from 'src/utils';
import { InvalidPDFDateStringError } from 'src/core/errors';
class PDFString extends PDFObject {
// The PDF spec allows newlines and parens to appear directly within a literal
// string. These character _may_ be escaped. But they do not _have_ to be. So
// for simplicity, we will not bother escaping them.
static of = (value: string) => new PDFString(value);
static fromDate = (date: Date) => {
const year = padStart(String(date.getUTCFullYear()), 4, '0');
const month = padStart(String(date.getUTCMonth() + 1), 2, '0');
const day = padStart(String(date.getUTCDate()), 2, '0');
const hours = padStart(String(date.getUTCHours()), 2, '0');
const mins = padStart(String(date.getUTCMinutes()), 2, '0');
const secs = padStart(String(date.getUTCSeconds()), 2, '0');
return new PDFString(`D:${year}${month}${day}${hours}${mins}${secs}Z`);
};
private readonly value: string;
private constructor(value: string) {
super();
this.value = value;
}
asBytes(): Uint8Array {
const bytes: number[] = [];
let octal = '';
let escaped = false;
const pushByte = (byte?: number) => {
if (byte !== undefined) bytes.push(byte);
escaped = false;
};
for (let idx = 0, len = this.value.length; idx < len; idx++) {
const char = this.value[idx];
const byte = toCharCode(char);
const nextChar = this.value[idx + 1];
if (!escaped) {
if (byte === CharCodes.BackSlash) escaped = true;
else pushByte(byte);
} else {
if (byte === CharCodes.Newline) pushByte();
else if (byte === CharCodes.CarriageReturn) pushByte();
else if (byte === CharCodes.n) pushByte(CharCodes.Newline);
else if (byte === CharCodes.r) pushByte(CharCodes.CarriageReturn);
else if (byte === CharCodes.t) pushByte(CharCodes.Tab);
else if (byte === CharCodes.b) pushByte(CharCodes.Backspace);
else if (byte === CharCodes.f) pushByte(CharCodes.FormFeed);
else if (byte === CharCodes.LeftParen) pushByte(CharCodes.LeftParen);
else if (byte === CharCodes.RightParen) pushByte(CharCodes.RightParen);
else if (byte === CharCodes.Backspace) pushByte(CharCodes.BackSlash);
else if (byte >= CharCodes.Zero && byte <= CharCodes.Seven) {
octal += char;
if (octal.length === 3 || !(nextChar >= '0' && nextChar <= '7')) {
pushByte(parseInt(octal, 8));
octal = '';
}
} else {
pushByte(byte);
}
}
}
return new Uint8Array(bytes);
}
decodeText(): string {
const bytes = this.asBytes();
if (hasUtf16BOM(bytes)) return utf16Decode(bytes);
return pdfDocEncodingDecode(bytes);
}
decodeDate(): Date {
const text = this.decodeText();
const date = parseDate(text);
if (!date) throw new InvalidPDFDateStringError(text);
return date;
}
asString(): string {
return this.value;
}
clone(): PDFString {
return PDFString.of(this.value);
}
toString(): string {
return `(${this.value})`;
}
sizeInBytes(): number {
return this.value.length + 2;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
buffer[offset++] = CharCodes.LeftParen;
offset += copyStringIntoBuffer(this.value, buffer, offset);
buffer[offset++] = CharCodes.RightParen;
return this.value.length + 2;
}
}
export default PDFString;

79
node_modules/pdf-lib/src/core/operators/PDFOperator.ts generated vendored Normal file
View File

@@ -0,0 +1,79 @@
import PDFArray from 'src/core/objects/PDFArray';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFString from 'src/core/objects/PDFString';
import PDFOperatorNames from 'src/core/operators/PDFOperatorNames';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
import { copyStringIntoBuffer } from 'src/utils';
export type PDFOperatorArg =
| string
| PDFName
| PDFArray
| PDFNumber
| PDFString
| PDFHexString;
class PDFOperator {
static of = (name: PDFOperatorNames, args?: PDFOperatorArg[]) =>
new PDFOperator(name, args);
private readonly name: PDFOperatorNames;
private readonly args: PDFOperatorArg[];
private constructor(name: PDFOperatorNames, args?: PDFOperatorArg[]) {
this.name = name;
this.args = args || [];
}
clone(context?: PDFContext): PDFOperator {
const args = new Array(this.args.length);
for (let idx = 0, len = args.length; idx < len; idx++) {
const arg = this.args[idx];
args[idx] = arg instanceof PDFObject ? arg.clone(context) : arg;
}
return PDFOperator.of(this.name, args);
}
toString(): string {
let value = '';
for (let idx = 0, len = this.args.length; idx < len; idx++) {
value += String(this.args[idx]) + ' ';
}
value += this.name;
return value;
}
sizeInBytes(): number {
let size = 0;
for (let idx = 0, len = this.args.length; idx < len; idx++) {
const arg = this.args[idx];
size += (arg instanceof PDFObject ? arg.sizeInBytes() : arg.length) + 1;
}
size += this.name.length;
return size;
}
copyBytesInto(buffer: Uint8Array, offset: number): number {
const initialOffset = offset;
for (let idx = 0, len = this.args.length; idx < len; idx++) {
const arg = this.args[idx];
if (arg instanceof PDFObject) {
offset += arg.copyBytesInto(buffer, offset);
} else {
offset += copyStringIntoBuffer(arg, buffer, offset);
}
buffer[offset++] = CharCodes.Space;
}
offset += copyStringIntoBuffer(this.name, buffer, offset);
return offset - initialOffset;
}
}
export default PDFOperator;

View File

@@ -0,0 +1,92 @@
enum PDFOperatorNames {
// Non Stroking Color Operators
NonStrokingColor = 'sc',
NonStrokingColorN = 'scn',
NonStrokingColorRgb = 'rg',
NonStrokingColorGray = 'g',
NonStrokingColorCmyk = 'k',
NonStrokingColorspace = 'cs',
// Stroking Color Operators
StrokingColor = 'SC',
StrokingColorN = 'SCN',
StrokingColorRgb = 'RG',
StrokingColorGray = 'G',
StrokingColorCmyk = 'K',
StrokingColorspace = 'CS',
// Marked Content Operators
BeginMarkedContentSequence = 'BDC',
BeginMarkedContent = 'BMC',
EndMarkedContent = 'EMC',
MarkedContentPointWithProps = 'DP',
MarkedContentPoint = 'MP',
DrawObject = 'Do',
// Graphics State Operators
ConcatTransformationMatrix = 'cm',
PopGraphicsState = 'Q',
PushGraphicsState = 'q',
SetFlatness = 'i',
SetGraphicsStateParams = 'gs',
SetLineCapStyle = 'J',
SetLineDashPattern = 'd',
SetLineJoinStyle = 'j',
SetLineMiterLimit = 'M',
SetLineWidth = 'w',
SetTextMatrix = 'Tm',
SetRenderingIntent = 'ri',
// Graphics Operators
AppendRectangle = 're',
BeginInlineImage = 'BI',
BeginInlineImageData = 'ID',
EndInlineImage = 'EI',
ClipEvenOdd = 'W*',
ClipNonZero = 'W',
CloseAndStroke = 's',
CloseFillEvenOddAndStroke = 'b*',
CloseFillNonZeroAndStroke = 'b',
ClosePath = 'h',
AppendBezierCurve = 'c',
CurveToReplicateFinalPoint = 'y',
CurveToReplicateInitialPoint = 'v',
EndPath = 'n',
FillEvenOddAndStroke = 'B*',
FillEvenOdd = 'f*',
FillNonZeroAndStroke = 'B',
FillNonZero = 'f',
LegacyFillNonZero = 'F',
LineTo = 'l',
MoveTo = 'm',
ShadingFill = 'sh',
StrokePath = 'S',
// Text Operators
BeginText = 'BT',
EndText = 'ET',
MoveText = 'Td',
MoveTextSetLeading = 'TD',
NextLine = 'T*',
SetCharacterSpacing = 'Tc',
SetFontAndSize = 'Tf',
SetTextHorizontalScaling = 'Tz',
SetTextLineHeight = 'TL',
SetTextRenderingMode = 'Tr',
SetTextRise = 'Ts',
SetWordSpacing = 'Tw',
ShowText = 'Tj',
ShowTextAdjusted = 'TJ',
ShowTextLine = "'", // tslint:disable-line quotemark
ShowTextLineAndSpace = '"',
// Type3 Font Operators
Type3D0 = 'd0',
Type3D1 = 'd1',
// Compatibility Section Operators
BeginCompatibilitySection = 'BX',
EndCompatibilitySection = 'EX',
}
export default PDFOperatorNames;

119
node_modules/pdf-lib/src/core/parser/BaseParser.ts generated vendored Normal file
View File

@@ -0,0 +1,119 @@
import { NumberParsingError } from 'src/core/errors';
import ByteStream from 'src/core/parser/ByteStream';
import CharCodes from 'src/core/syntax/CharCodes';
import { IsDigit, IsNumeric } from 'src/core/syntax/Numeric';
import { IsWhitespace } from 'src/core/syntax/Whitespace';
import { charFromCode } from 'src/utils';
const { Newline, CarriageReturn } = CharCodes;
// TODO: Throw error if eof is reached before finishing object parse...
class BaseParser {
protected readonly bytes: ByteStream;
protected readonly capNumbers: boolean;
constructor(bytes: ByteStream, capNumbers = false) {
this.bytes = bytes;
this.capNumbers = capNumbers;
}
protected parseRawInt(): number {
let value = '';
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (!IsDigit[byte]) break;
value += charFromCode(this.bytes.next());
}
const numberValue = Number(value);
if (!value || !isFinite(numberValue)) {
throw new NumberParsingError(this.bytes.position(), value);
}
return numberValue;
}
// TODO: Maybe handle exponential format?
// TODO: Compare performance of string concatenation to charFromCode(...bytes)
protected parseRawNumber(): number {
let value = '';
// Parse integer-part, the leading (+ | - | . | 0-9)
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (!IsNumeric[byte]) break;
value += charFromCode(this.bytes.next());
if (byte === CharCodes.Period) break;
}
// Parse decimal-part, the trailing (0-9)
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (!IsDigit[byte]) break;
value += charFromCode(this.bytes.next());
}
const numberValue = Number(value);
if (!value || !isFinite(numberValue)) {
throw new NumberParsingError(this.bytes.position(), value);
}
if (numberValue > Number.MAX_SAFE_INTEGER) {
if (this.capNumbers) {
const msg = `Parsed number that is too large for some PDF readers: ${value}, using Number.MAX_SAFE_INTEGER instead.`;
console.warn(msg);
return Number.MAX_SAFE_INTEGER;
} else {
const msg = `Parsed number that is too large for some PDF readers: ${value}, not capping.`;
console.warn(msg);
}
}
return numberValue;
}
protected skipWhitespace(): void {
while (!this.bytes.done() && IsWhitespace[this.bytes.peek()]) {
this.bytes.next();
}
}
protected skipLine(): void {
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (byte === Newline || byte === CarriageReturn) return;
this.bytes.next();
}
}
protected skipComment(): boolean {
if (this.bytes.peek() !== CharCodes.Percent) return false;
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (byte === Newline || byte === CarriageReturn) return true;
this.bytes.next();
}
return true;
}
protected skipWhitespaceAndComments(): void {
this.skipWhitespace();
while (this.skipComment()) this.skipWhitespace();
}
protected matchKeyword(keyword: number[]): boolean {
const initialOffset = this.bytes.offset();
for (let idx = 0, len = keyword.length; idx < len; idx++) {
if (this.bytes.done() || this.bytes.next() !== keyword[idx]) {
this.bytes.moveTo(initialOffset);
return false;
}
}
return true;
}
}
export default BaseParser;

76
node_modules/pdf-lib/src/core/parser/ByteStream.ts generated vendored Normal file
View File

@@ -0,0 +1,76 @@
import { NextByteAssertionError } from 'src/core/errors';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import { decodePDFRawStream } from 'src/core/streams/decode';
import CharCodes from 'src/core/syntax/CharCodes';
// TODO: See how line/col tracking affects performance
class ByteStream {
static of = (bytes: Uint8Array) => new ByteStream(bytes);
static fromPDFRawStream = (rawStream: PDFRawStream) =>
ByteStream.of(decodePDFRawStream(rawStream).decode());
private readonly bytes: Uint8Array;
private readonly length: number;
private idx = 0;
private line = 0;
private column = 0;
constructor(bytes: Uint8Array) {
this.bytes = bytes;
this.length = this.bytes.length;
}
moveTo(offset: number): void {
this.idx = offset;
}
next(): number {
const byte = this.bytes[this.idx++];
if (byte === CharCodes.Newline) {
this.line += 1;
this.column = 0;
} else {
this.column += 1;
}
return byte;
}
assertNext(expected: number): number {
if (this.peek() !== expected) {
throw new NextByteAssertionError(this.position(), expected, this.peek());
}
return this.next();
}
peek(): number {
return this.bytes[this.idx];
}
peekAhead(steps: number) {
return this.bytes[this.idx + steps];
}
peekAt(offset: number) {
return this.bytes[offset];
}
done(): boolean {
return this.idx >= this.length;
}
offset(): number {
return this.idx;
}
slice(start: number, end: number): Uint8Array {
return this.bytes.slice(start, end);
}
position(): { line: number; column: number; offset: number } {
return { line: this.line, column: this.column, offset: this.idx };
}
}
export default ByteStream;

274
node_modules/pdf-lib/src/core/parser/PDFObjectParser.ts generated vendored Normal file
View File

@@ -0,0 +1,274 @@
import {
PDFObjectParsingError,
PDFStreamParsingError,
Position,
UnbalancedParenthesisError,
} from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFBool from 'src/core/objects/PDFBool';
import PDFDict, { DictMap } from 'src/core/objects/PDFDict';
import PDFHexString from 'src/core/objects/PDFHexString';
import PDFName from 'src/core/objects/PDFName';
import PDFNull from 'src/core/objects/PDFNull';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import PDFStream from 'src/core/objects/PDFStream';
import PDFString from 'src/core/objects/PDFString';
import BaseParser from 'src/core/parser/BaseParser';
import ByteStream from 'src/core/parser/ByteStream';
import PDFContext from 'src/core/PDFContext';
import PDFCatalog from 'src/core/structures/PDFCatalog';
import PDFPageLeaf from 'src/core/structures/PDFPageLeaf';
import PDFPageTree from 'src/core/structures/PDFPageTree';
import CharCodes from 'src/core/syntax/CharCodes';
import { IsDelimiter } from 'src/core/syntax/Delimiters';
import { Keywords } from 'src/core/syntax/Keywords';
import { IsDigit, IsNumeric } from 'src/core/syntax/Numeric';
import { IsWhitespace } from 'src/core/syntax/Whitespace';
import { charFromCode } from 'src/utils';
// TODO: Throw error if eof is reached before finishing object parse...
class PDFObjectParser extends BaseParser {
static forBytes = (
bytes: Uint8Array,
context: PDFContext,
capNumbers?: boolean,
) => new PDFObjectParser(ByteStream.of(bytes), context, capNumbers);
static forByteStream = (
byteStream: ByteStream,
context: PDFContext,
capNumbers = false,
) => new PDFObjectParser(byteStream, context, capNumbers);
protected readonly context: PDFContext;
constructor(byteStream: ByteStream, context: PDFContext, capNumbers = false) {
super(byteStream, capNumbers);
this.context = context;
}
// TODO: Is it possible to reduce duplicate parsing for ref lookaheads?
parseObject(): PDFObject {
this.skipWhitespaceAndComments();
if (this.matchKeyword(Keywords.true)) return PDFBool.True;
if (this.matchKeyword(Keywords.false)) return PDFBool.False;
if (this.matchKeyword(Keywords.null)) return PDFNull;
const byte = this.bytes.peek();
if (
byte === CharCodes.LessThan &&
this.bytes.peekAhead(1) === CharCodes.LessThan
) {
return this.parseDictOrStream();
}
if (byte === CharCodes.LessThan) return this.parseHexString();
if (byte === CharCodes.LeftParen) return this.parseString();
if (byte === CharCodes.ForwardSlash) return this.parseName();
if (byte === CharCodes.LeftSquareBracket) return this.parseArray();
if (IsNumeric[byte]) return this.parseNumberOrRef();
throw new PDFObjectParsingError(this.bytes.position(), byte);
}
protected parseNumberOrRef(): PDFNumber | PDFRef {
const firstNum = this.parseRawNumber();
this.skipWhitespaceAndComments();
const lookaheadStart = this.bytes.offset();
if (IsDigit[this.bytes.peek()]) {
const secondNum = this.parseRawNumber();
this.skipWhitespaceAndComments();
if (this.bytes.peek() === CharCodes.R) {
this.bytes.assertNext(CharCodes.R);
return PDFRef.of(firstNum, secondNum);
}
}
this.bytes.moveTo(lookaheadStart);
return PDFNumber.of(firstNum);
}
// TODO: Maybe update PDFHexString.of() logic to remove whitespace and validate input?
protected parseHexString(): PDFHexString {
let value = '';
this.bytes.assertNext(CharCodes.LessThan);
while (!this.bytes.done() && this.bytes.peek() !== CharCodes.GreaterThan) {
value += charFromCode(this.bytes.next());
}
this.bytes.assertNext(CharCodes.GreaterThan);
return PDFHexString.of(value);
}
protected parseString(): PDFString {
let nestingLvl = 0;
let isEscaped = false;
let value = '';
while (!this.bytes.done()) {
const byte = this.bytes.next();
value += charFromCode(byte);
// Check for unescaped parenthesis
if (!isEscaped) {
if (byte === CharCodes.LeftParen) nestingLvl += 1;
if (byte === CharCodes.RightParen) nestingLvl -= 1;
}
// Track whether current character is being escaped or not
if (byte === CharCodes.BackSlash) {
isEscaped = !isEscaped;
} else if (isEscaped) {
isEscaped = false;
}
// Once (if) the unescaped parenthesis balance out, return their contents
if (nestingLvl === 0) {
// Remove the outer parens so they aren't part of the contents
return PDFString.of(value.substring(1, value.length - 1));
}
}
throw new UnbalancedParenthesisError(this.bytes.position());
}
// TODO: Compare performance of string concatenation to charFromCode(...bytes)
// TODO: Maybe preallocate small Uint8Array if can use charFromCode?
protected parseName(): PDFName {
this.bytes.assertNext(CharCodes.ForwardSlash);
let name = '';
while (!this.bytes.done()) {
const byte = this.bytes.peek();
if (IsWhitespace[byte] || IsDelimiter[byte]) break;
name += charFromCode(byte);
this.bytes.next();
}
return PDFName.of(name);
}
protected parseArray(): PDFArray {
this.bytes.assertNext(CharCodes.LeftSquareBracket);
this.skipWhitespaceAndComments();
const pdfArray = PDFArray.withContext(this.context);
while (this.bytes.peek() !== CharCodes.RightSquareBracket) {
const element = this.parseObject();
pdfArray.push(element);
this.skipWhitespaceAndComments();
}
this.bytes.assertNext(CharCodes.RightSquareBracket);
return pdfArray;
}
protected parseDict(): PDFDict {
this.bytes.assertNext(CharCodes.LessThan);
this.bytes.assertNext(CharCodes.LessThan);
this.skipWhitespaceAndComments();
const dict: DictMap = new Map();
while (
!this.bytes.done() &&
this.bytes.peek() !== CharCodes.GreaterThan &&
this.bytes.peekAhead(1) !== CharCodes.GreaterThan
) {
const key = this.parseName();
const value = this.parseObject();
dict.set(key, value);
this.skipWhitespaceAndComments();
}
this.skipWhitespaceAndComments();
this.bytes.assertNext(CharCodes.GreaterThan);
this.bytes.assertNext(CharCodes.GreaterThan);
const Type = dict.get(PDFName.of('Type'));
if (Type === PDFName.of('Catalog')) {
return PDFCatalog.fromMapWithContext(dict, this.context);
} else if (Type === PDFName.of('Pages')) {
return PDFPageTree.fromMapWithContext(dict, this.context);
} else if (Type === PDFName.of('Page')) {
return PDFPageLeaf.fromMapWithContext(dict, this.context);
} else {
return PDFDict.fromMapWithContext(dict, this.context);
}
}
protected parseDictOrStream(): PDFDict | PDFStream {
const startPos = this.bytes.position();
const dict = this.parseDict();
this.skipWhitespaceAndComments();
if (
!this.matchKeyword(Keywords.streamEOF1) &&
!this.matchKeyword(Keywords.streamEOF2) &&
!this.matchKeyword(Keywords.streamEOF3) &&
!this.matchKeyword(Keywords.streamEOF4) &&
!this.matchKeyword(Keywords.stream)
) {
return dict;
}
const start = this.bytes.offset();
let end: number;
const Length = dict.get(PDFName.of('Length'));
if (Length instanceof PDFNumber) {
end = start + Length.asNumber();
this.bytes.moveTo(end);
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.endstream)) {
this.bytes.moveTo(start);
end = this.findEndOfStreamFallback(startPos);
}
} else {
end = this.findEndOfStreamFallback(startPos);
}
const contents = this.bytes.slice(start, end);
return PDFRawStream.of(dict, contents);
}
protected findEndOfStreamFallback(startPos: Position) {
// Move to end of stream, while handling nested streams
let nestingLvl = 1;
let end = this.bytes.offset();
while (!this.bytes.done()) {
end = this.bytes.offset();
if (this.matchKeyword(Keywords.stream)) {
nestingLvl += 1;
} else if (
this.matchKeyword(Keywords.EOF1endstream) ||
this.matchKeyword(Keywords.EOF2endstream) ||
this.matchKeyword(Keywords.EOF3endstream) ||
this.matchKeyword(Keywords.endstream)
) {
nestingLvl -= 1;
} else {
this.bytes.next();
}
if (nestingLvl === 0) break;
}
if (nestingLvl !== 0) throw new PDFStreamParsingError(startPos);
return end;
}
}
export default PDFObjectParser;

View File

@@ -0,0 +1,67 @@
import { ReparseError } from 'src/core/errors';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import ByteStream from 'src/core/parser/ByteStream';
import PDFObjectParser from 'src/core/parser/PDFObjectParser';
import { waitForTick } from 'src/utils';
class PDFObjectStreamParser extends PDFObjectParser {
static forStream = (
rawStream: PDFRawStream,
shouldWaitForTick?: () => boolean,
) => new PDFObjectStreamParser(rawStream, shouldWaitForTick);
private alreadyParsed: boolean;
private readonly shouldWaitForTick: () => boolean;
private readonly firstOffset: number;
private readonly objectCount: number;
constructor(rawStream: PDFRawStream, shouldWaitForTick?: () => boolean) {
super(ByteStream.fromPDFRawStream(rawStream), rawStream.dict.context);
const { dict } = rawStream;
this.alreadyParsed = false;
this.shouldWaitForTick = shouldWaitForTick || (() => false);
this.firstOffset = dict.lookup(PDFName.of('First'), PDFNumber).asNumber();
this.objectCount = dict.lookup(PDFName.of('N'), PDFNumber).asNumber();
}
async parseIntoContext(): Promise<void> {
if (this.alreadyParsed) {
throw new ReparseError('PDFObjectStreamParser', 'parseIntoContext');
}
this.alreadyParsed = true;
const offsetsAndObjectNumbers = this.parseOffsetsAndObjectNumbers();
for (let idx = 0, len = offsetsAndObjectNumbers.length; idx < len; idx++) {
const { objectNumber, offset } = offsetsAndObjectNumbers[idx];
this.bytes.moveTo(this.firstOffset + offset);
const object = this.parseObject();
const ref = PDFRef.of(objectNumber, 0);
this.context.assign(ref, object);
if (this.shouldWaitForTick()) await waitForTick();
}
}
private parseOffsetsAndObjectNumbers(): {
objectNumber: number;
offset: number;
}[] {
const offsetsAndObjectNumbers = [];
for (let idx = 0, len = this.objectCount; idx < len; idx++) {
this.skipWhitespaceAndComments();
const objectNumber = this.parseRawInt();
this.skipWhitespaceAndComments();
const offset = this.parseRawInt();
offsetsAndObjectNumbers.push({ objectNumber, offset });
}
return offsetsAndObjectNumbers;
}
}
export default PDFObjectStreamParser;

364
node_modules/pdf-lib/src/core/parser/PDFParser.ts generated vendored Normal file
View File

@@ -0,0 +1,364 @@
import PDFCrossRefSection from 'src/core/document/PDFCrossRefSection';
import PDFHeader from 'src/core/document/PDFHeader';
import PDFTrailer from 'src/core/document/PDFTrailer';
import {
MissingKeywordError,
MissingPDFHeaderError,
PDFInvalidObjectParsingError,
ReparseError,
StalledParserError,
} from 'src/core/errors';
import PDFDict from 'src/core/objects/PDFDict';
import PDFInvalidObject from 'src/core/objects/PDFInvalidObject';
import PDFName from 'src/core/objects/PDFName';
import PDFObject from 'src/core/objects/PDFObject';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import ByteStream from 'src/core/parser/ByteStream';
import PDFObjectParser from 'src/core/parser/PDFObjectParser';
import PDFObjectStreamParser from 'src/core/parser/PDFObjectStreamParser';
import PDFXRefStreamParser from 'src/core/parser/PDFXRefStreamParser';
import PDFContext from 'src/core/PDFContext';
import CharCodes from 'src/core/syntax/CharCodes';
import { Keywords } from 'src/core/syntax/Keywords';
import { IsDigit } from 'src/core/syntax/Numeric';
import { waitForTick } from 'src/utils';
class PDFParser extends PDFObjectParser {
static forBytesWithOptions = (
pdfBytes: Uint8Array,
objectsPerTick?: number,
throwOnInvalidObject?: boolean,
capNumbers?: boolean,
) =>
new PDFParser(pdfBytes, objectsPerTick, throwOnInvalidObject, capNumbers);
private readonly objectsPerTick: number;
private readonly throwOnInvalidObject: boolean;
private alreadyParsed = false;
private parsedObjects = 0;
constructor(
pdfBytes: Uint8Array,
objectsPerTick = Infinity,
throwOnInvalidObject = false,
capNumbers = false,
) {
super(ByteStream.of(pdfBytes), PDFContext.create(), capNumbers);
this.objectsPerTick = objectsPerTick;
this.throwOnInvalidObject = throwOnInvalidObject;
}
async parseDocument(): Promise<PDFContext> {
if (this.alreadyParsed) {
throw new ReparseError('PDFParser', 'parseDocument');
}
this.alreadyParsed = true;
this.context.header = this.parseHeader();
let prevOffset;
while (!this.bytes.done()) {
await this.parseDocumentSection();
const offset = this.bytes.offset();
if (offset === prevOffset) {
throw new StalledParserError(this.bytes.position());
}
prevOffset = offset;
}
this.maybeRecoverRoot();
if (this.context.lookup(PDFRef.of(0))) {
console.warn('Removing parsed object: 0 0 R');
this.context.delete(PDFRef.of(0));
}
return this.context;
}
private maybeRecoverRoot(): void {
const isValidCatalog = (obj?: PDFObject) =>
obj instanceof PDFDict &&
obj.lookup(PDFName.of('Type')) === PDFName.of('Catalog');
const catalog = this.context.lookup(this.context.trailerInfo.Root);
if (!isValidCatalog(catalog)) {
const indirectObjects = this.context.enumerateIndirectObjects();
for (let idx = 0, len = indirectObjects.length; idx < len; idx++) {
const [ref, object] = indirectObjects[idx];
if (isValidCatalog(object)) {
this.context.trailerInfo.Root = ref;
}
}
}
}
private parseHeader(): PDFHeader {
while (!this.bytes.done()) {
if (this.matchKeyword(Keywords.header)) {
const major = this.parseRawInt();
this.bytes.assertNext(CharCodes.Period);
const minor = this.parseRawInt();
const header = PDFHeader.forVersion(major, minor);
this.skipBinaryHeaderComment();
return header;
}
this.bytes.next();
}
throw new MissingPDFHeaderError(this.bytes.position());
}
private parseIndirectObjectHeader(): PDFRef {
this.skipWhitespaceAndComments();
const objectNumber = this.parseRawInt();
this.skipWhitespaceAndComments();
const generationNumber = this.parseRawInt();
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.obj)) {
throw new MissingKeywordError(this.bytes.position(), Keywords.obj);
}
return PDFRef.of(objectNumber, generationNumber);
}
private matchIndirectObjectHeader(): boolean {
const initialOffset = this.bytes.offset();
try {
this.parseIndirectObjectHeader();
return true;
} catch (e) {
this.bytes.moveTo(initialOffset);
return false;
}
}
private shouldWaitForTick = () => {
this.parsedObjects += 1;
return this.parsedObjects % this.objectsPerTick === 0;
};
private async parseIndirectObject(): Promise<PDFRef> {
const ref = this.parseIndirectObjectHeader();
this.skipWhitespaceAndComments();
const object = this.parseObject();
this.skipWhitespaceAndComments();
// if (!this.matchKeyword(Keywords.endobj)) {
// throw new MissingKeywordError(this.bytes.position(), Keywords.endobj);
// }
// TODO: Log a warning if this fails...
this.matchKeyword(Keywords.endobj);
if (
object instanceof PDFRawStream &&
object.dict.lookup(PDFName.of('Type')) === PDFName.of('ObjStm')
) {
await PDFObjectStreamParser.forStream(
object,
this.shouldWaitForTick,
).parseIntoContext();
} else if (
object instanceof PDFRawStream &&
object.dict.lookup(PDFName.of('Type')) === PDFName.of('XRef')
) {
PDFXRefStreamParser.forStream(object).parseIntoContext();
} else {
this.context.assign(ref, object);
}
return ref;
}
// TODO: Improve and clean this up
private tryToParseInvalidIndirectObject() {
const startPos = this.bytes.position();
const msg = `Trying to parse invalid object: ${JSON.stringify(startPos)})`;
if (this.throwOnInvalidObject) throw new Error(msg);
console.warn(msg);
const ref = this.parseIndirectObjectHeader();
console.warn(`Invalid object ref: ${ref}`);
this.skipWhitespaceAndComments();
const start = this.bytes.offset();
let failed = true;
while (!this.bytes.done()) {
if (this.matchKeyword(Keywords.endobj)) {
failed = false;
}
if (!failed) break;
this.bytes.next();
}
if (failed) throw new PDFInvalidObjectParsingError(startPos);
const end = this.bytes.offset() - Keywords.endobj.length;
const object = PDFInvalidObject.of(this.bytes.slice(start, end));
this.context.assign(ref, object);
return ref;
}
private async parseIndirectObjects(): Promise<void> {
this.skipWhitespaceAndComments();
while (!this.bytes.done() && IsDigit[this.bytes.peek()]) {
const initialOffset = this.bytes.offset();
try {
await this.parseIndirectObject();
} catch (e) {
// TODO: Add tracing/logging mechanism to track when this happens!
this.bytes.moveTo(initialOffset);
this.tryToParseInvalidIndirectObject();
}
this.skipWhitespaceAndComments();
// TODO: Can this be done only when needed, to avoid harming performance?
this.skipJibberish();
if (this.shouldWaitForTick()) await waitForTick();
}
}
private maybeParseCrossRefSection(): PDFCrossRefSection | void {
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.xref)) return;
this.skipWhitespaceAndComments();
let objectNumber = -1;
const xref = PDFCrossRefSection.createEmpty();
while (!this.bytes.done() && IsDigit[this.bytes.peek()]) {
const firstInt = this.parseRawInt();
this.skipWhitespaceAndComments();
const secondInt = this.parseRawInt();
this.skipWhitespaceAndComments();
const byte = this.bytes.peek();
if (byte === CharCodes.n || byte === CharCodes.f) {
const ref = PDFRef.of(objectNumber, secondInt);
if (this.bytes.next() === CharCodes.n) {
xref.addEntry(ref, firstInt);
} else {
// this.context.delete(ref);
xref.addDeletedEntry(ref, firstInt);
}
objectNumber += 1;
} else {
objectNumber = firstInt;
}
this.skipWhitespaceAndComments();
}
return xref;
}
private maybeParseTrailerDict(): void {
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.trailer)) return;
this.skipWhitespaceAndComments();
const dict = this.parseDict();
const { context } = this;
context.trailerInfo = {
Root: dict.get(PDFName.of('Root')) || context.trailerInfo.Root,
Encrypt: dict.get(PDFName.of('Encrypt')) || context.trailerInfo.Encrypt,
Info: dict.get(PDFName.of('Info')) || context.trailerInfo.Info,
ID: dict.get(PDFName.of('ID')) || context.trailerInfo.ID,
};
}
private maybeParseTrailer(): PDFTrailer | void {
this.skipWhitespaceAndComments();
if (!this.matchKeyword(Keywords.startxref)) return;
this.skipWhitespaceAndComments();
const offset = this.parseRawInt();
this.skipWhitespace();
this.matchKeyword(Keywords.eof);
this.skipWhitespaceAndComments();
this.matchKeyword(Keywords.eof);
this.skipWhitespaceAndComments();
return PDFTrailer.forLastCrossRefSectionOffset(offset);
}
private async parseDocumentSection(): Promise<void> {
await this.parseIndirectObjects();
this.maybeParseCrossRefSection();
this.maybeParseTrailerDict();
this.maybeParseTrailer();
// TODO: Can this be done only when needed, to avoid harming performance?
this.skipJibberish();
}
/**
* This operation is not necessary for valid PDF files. But some invalid PDFs
* contain jibberish in between indirect objects. This method is designed to
* skip past that jibberish, should it exist, until it reaches the next
* indirect object header, an xref table section, or the file trailer.
*/
private skipJibberish(): void {
this.skipWhitespaceAndComments();
while (!this.bytes.done()) {
const initialOffset = this.bytes.offset();
const byte = this.bytes.peek();
const isAlphaNumeric = byte >= CharCodes.Space && byte <= CharCodes.Tilde;
if (isAlphaNumeric) {
if (
this.matchKeyword(Keywords.xref) ||
this.matchKeyword(Keywords.trailer) ||
this.matchKeyword(Keywords.startxref) ||
this.matchIndirectObjectHeader()
) {
this.bytes.moveTo(initialOffset);
break;
}
}
this.bytes.next();
}
}
/**
* Skips the binary comment following a PDF header. The specification
* defines this binary comment (section 7.5.2 File Header) as a sequence of 4
* or more bytes that are 128 or greater, and which are preceded by a "%".
*
* This would imply that to strip out this binary comment, we could check for
* a sequence of bytes starting with "%", and remove all subsequent bytes that
* are 128 or greater. This works for many documents that properly comply with
* the spec. But in the wild, there are PDFs that omit the leading "%", and
* include bytes that are less than 128 (e.g. 0 or 1). So in order to parse
* these headers correctly, we just throw out all bytes leading up to the
* first indirect object header.
*/
private skipBinaryHeaderComment(): void {
this.skipWhitespaceAndComments();
try {
const initialOffset = this.bytes.offset();
this.parseIndirectObjectHeader();
this.bytes.moveTo(initialOffset);
} catch (e) {
this.bytes.next();
this.skipWhitespaceAndComments();
}
}
}
export default PDFParser;

View File

@@ -0,0 +1,130 @@
import { ReparseError } from 'src/core/errors';
import PDFArray from 'src/core/objects/PDFArray';
import PDFDict from 'src/core/objects/PDFDict';
import PDFName from 'src/core/objects/PDFName';
import PDFNumber from 'src/core/objects/PDFNumber';
import PDFRawStream from 'src/core/objects/PDFRawStream';
import PDFRef from 'src/core/objects/PDFRef';
import ByteStream from 'src/core/parser/ByteStream';
import PDFContext from 'src/core/PDFContext';
export interface Entry {
ref: PDFRef;
offset: number;
deleted: boolean;
inObjectStream: boolean;
}
class PDFXRefStreamParser {
static forStream = (rawStream: PDFRawStream) =>
new PDFXRefStreamParser(rawStream);
private alreadyParsed: boolean;
private readonly dict: PDFDict;
private readonly context: PDFContext;
private readonly bytes: ByteStream;
private readonly subsections: {
firstObjectNumber: number;
length: number;
}[];
private readonly byteWidths: [number, number, number];
constructor(rawStream: PDFRawStream) {
this.alreadyParsed = false;
this.dict = rawStream.dict;
this.bytes = ByteStream.fromPDFRawStream(rawStream);
this.context = this.dict.context;
const Size = this.dict.lookup(PDFName.of('Size'), PDFNumber);
const Index = this.dict.lookup(PDFName.of('Index'));
if (Index instanceof PDFArray) {
this.subsections = [];
for (let idx = 0, len = Index.size(); idx < len; idx += 2) {
const firstObjectNumber = Index.lookup(idx + 0, PDFNumber).asNumber();
const length = Index.lookup(idx + 1, PDFNumber).asNumber();
this.subsections.push({ firstObjectNumber, length });
}
} else {
this.subsections = [{ firstObjectNumber: 0, length: Size.asNumber() }];
}
const W = this.dict.lookup(PDFName.of('W'), PDFArray);
this.byteWidths = [-1, -1, -1];
for (let idx = 0, len = W.size(); idx < len; idx++) {
this.byteWidths[idx] = W.lookup(idx, PDFNumber).asNumber();
}
}
parseIntoContext(): Entry[] {
if (this.alreadyParsed) {
throw new ReparseError('PDFXRefStreamParser', 'parseIntoContext');
}
this.alreadyParsed = true;
this.context.trailerInfo = {
Root: this.dict.get(PDFName.of('Root')),
Encrypt: this.dict.get(PDFName.of('Encrypt')),
Info: this.dict.get(PDFName.of('Info')),
ID: this.dict.get(PDFName.of('ID')),
};
const entries = this.parseEntries();
// for (let idx = 0, len = entries.length; idx < len; idx++) {
// const entry = entries[idx];
// if (entry.deleted) this.context.delete(entry.ref);
// }
return entries;
}
private parseEntries(): Entry[] {
const entries = [];
const [typeFieldWidth, offsetFieldWidth, genFieldWidth] = this.byteWidths;
for (
let subsectionIdx = 0, subsectionLen = this.subsections.length;
subsectionIdx < subsectionLen;
subsectionIdx++
) {
const { firstObjectNumber, length } = this.subsections[subsectionIdx];
for (let objIdx = 0; objIdx < length; objIdx++) {
let type = 0;
for (let idx = 0, len = typeFieldWidth; idx < len; idx++) {
type = (type << 8) | this.bytes.next();
}
let offset = 0;
for (let idx = 0, len = offsetFieldWidth; idx < len; idx++) {
offset = (offset << 8) | this.bytes.next();
}
let generationNumber = 0;
for (let idx = 0, len = genFieldWidth; idx < len; idx++) {
generationNumber = (generationNumber << 8) | this.bytes.next();
}
// When the `type` field is absent, it defaults to 1
if (typeFieldWidth === 0) type = 1;
const objectNumber = firstObjectNumber + objIdx;
const entry = {
ref: PDFRef.of(objectNumber, generationNumber),
offset,
deleted: type === 0,
inObjectStream: type === 2,
};
entries.push(entry);
}
}
return entries;
}
}
export default PDFXRefStreamParser;

98
node_modules/pdf-lib/src/core/streams/Ascii85Stream.ts generated vendored Normal file
View File

@@ -0,0 +1,98 @@
/*
* Copyright 2012 Mozilla Foundation
*
* The Ascii85Stream class contained in this file is a TypeScript port of the
* JavaScript Ascii85Stream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
import DecodeStream from 'src/core/streams/DecodeStream';
import { StreamType } from 'src/core/streams/Stream';
const isSpace = (ch: number) =>
ch === 0x20 || ch === 0x09 || ch === 0x0d || ch === 0x0a;
class Ascii85Stream extends DecodeStream {
private stream: StreamType;
private input: Uint8Array;
constructor(stream: StreamType, maybeLength?: number) {
super(maybeLength);
this.stream = stream;
this.input = new Uint8Array(5);
// Most streams increase in size when decoded, but Ascii85 streams
// typically shrink by ~20%.
if (maybeLength) {
maybeLength = 0.8 * maybeLength;
}
}
protected readBlock() {
const TILDA_CHAR = 0x7e; // '~'
const Z_LOWER_CHAR = 0x7a; // 'z'
const EOF = -1;
const stream = this.stream;
let c = stream.getByte();
while (isSpace(c)) {
c = stream.getByte();
}
if (c === EOF || c === TILDA_CHAR) {
this.eof = true;
return;
}
const bufferLength = this.bufferLength;
let buffer;
let i;
// special code for z
if (c === Z_LOWER_CHAR) {
buffer = this.ensureBuffer(bufferLength + 4);
for (i = 0; i < 4; ++i) {
buffer[bufferLength + i] = 0;
}
this.bufferLength += 4;
} else {
const input = this.input;
input[0] = c;
for (i = 1; i < 5; ++i) {
c = stream.getByte();
while (isSpace(c)) {
c = stream.getByte();
}
input[i] = c;
if (c === EOF || c === TILDA_CHAR) {
break;
}
}
buffer = this.ensureBuffer(bufferLength + i - 1);
this.bufferLength += i - 1;
// partial ending;
if (i < 5) {
for (; i < 5; ++i) {
input[i] = 0x21 + 84;
}
this.eof = true;
}
let t = 0;
for (i = 0; i < 5; ++i) {
t = t * 85 + (input[i] - 0x21);
}
for (i = 3; i >= 0; --i) {
buffer[bufferLength + i] = t & 0xff;
t >>= 8;
}
}
}
}
export default Ascii85Stream;

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012 Mozilla Foundation
*
* The AsciiHexStream class contained in this file is a TypeScript port of the
* JavaScript AsciiHexStream class in Mozilla's pdf.js project, made available
* under the Apache 2.0 open source license.
*/
import DecodeStream from 'src/core/streams/DecodeStream';
import { StreamType } from 'src/core/streams/Stream';
class AsciiHexStream extends DecodeStream {
private stream: StreamType;
private firstDigit: number;
constructor(stream: StreamType, maybeLength?: number) {
super(maybeLength);
this.stream = stream;
this.firstDigit = -1;
// Most streams increase in size when decoded, but AsciiHex streams shrink
// by 50%.
if (maybeLength) {
maybeLength = 0.5 * maybeLength;
}
}
protected readBlock() {
const UPSTREAM_BLOCK_SIZE = 8000;
const bytes = this.stream.getBytes(UPSTREAM_BLOCK_SIZE);
if (!bytes.length) {
this.eof = true;
return;
}
const maxDecodeLength = (bytes.length + 1) >> 1;
const buffer = this.ensureBuffer(this.bufferLength + maxDecodeLength);
let bufferLength = this.bufferLength;
let firstDigit = this.firstDigit;
for (let i = 0, ii = bytes.length; i < ii; i++) {
const ch = bytes[i];
let digit;
if (ch >= 0x30 && ch <= 0x39) {
// '0'-'9'
digit = ch & 0x0f;
} else if ((ch >= 0x41 && ch <= 0x46) || (ch >= 0x61 && ch <= 0x66)) {
// 'A'-'Z', 'a'-'z'
digit = (ch & 0x0f) + 9;
} else if (ch === 0x3e) {
// '>'
this.eof = true;
break;
} else {
// probably whitespace
continue; // ignoring
}
if (firstDigit < 0) {
firstDigit = digit;
} else {
buffer[bufferLength++] = (firstDigit << 4) | digit;
firstDigit = -1;
}
}
if (firstDigit >= 0 && this.eof) {
// incomplete byte
buffer[bufferLength++] = firstDigit << 4;
firstDigit = -1;
}
this.firstDigit = firstDigit;
this.bufferLength = bufferLength;
}
}
export default AsciiHexStream;

Some files were not shown because too many files have changed in this diff Show More