first commit
This commit is contained in:
521
node_modules/pdf-lib/src/api/form/PDFField.ts
generated
vendored
Normal file
521
node_modules/pdf-lib/src/api/form/PDFField.ts
generated
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user