Skip to content

Commit

Permalink
Add image stamping for TextField elements (#718)
Browse files Browse the repository at this point in the history
* Add image stamping for TextField elements (#700)

* Add image stamping for TextField elements

* - Add `ImageAlignment`
- Add test for image stamping for TextField elements

* Cleanup

Co-authored-by: Bj Tecu <[email protected]>
  • Loading branch information
Hopding and btecu authored Dec 20, 2020
1 parent ad8858f commit 94f48b0
Show file tree
Hide file tree
Showing 12 changed files with 144 additions and 71 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ const traitsField = form.getTextField('Feat+Traits')
const treasureField = form.getTextField('Treasure')

const characterImageField = form.getButton('CHARACTER IMAGE')
const factionImageField = form.getButton('Faction Symbol Image')
const factionImageField = form.getTextField('Faction Symbol Image')

// Fill in the basic info fields
nameField.setText('Mario')
Expand Down
2 changes: 1 addition & 1 deletion apps/deno/tests/test15.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default async (assets: Assets) => {
);

form.getTextField('FactionName').setText(`Mario's Emblem`);
form.getButton('Faction Symbol Image').setImage(emblemImage);
form.getTextField('Faction Symbol Image').setImage(emblemImage);

form
.getTextField('Backstory')
Expand Down
2 changes: 1 addition & 1 deletion apps/node/tests/test15.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default async (assets: Assets) => {
);

form.getTextField('FactionName').setText(`Mario's Emblem`);
form.getButton('Faction Symbol Image').setImage(emblemImage);
form.getTextField('Faction Symbol Image').setImage(emblemImage);

form
.getTextField('Backstory')
Expand Down
2 changes: 1 addition & 1 deletion apps/rn/src/tests/test15.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default async () => {
);

form.getTextField('FactionName').setText(`Mario's Emblem`);
form.getButton('Faction Symbol Image').setImage(emblemImage);
form.getTextField('Faction Symbol Image').setImage(emblemImage);

form
.getTextField('Backstory')
Expand Down
2 changes: 1 addition & 1 deletion apps/web/test15.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
);

form.getTextField('FactionName').setText(`Mario's Emblem`);
form.getButton('Faction Symbol Image').setImage(emblemImage);
form.getTextField('Faction Symbol Image').setImage(emblemImage);

form
.getTextField('Backstory')
Expand Down
Binary file modified assets/pdfs/dod_character.pdf
Binary file not shown.
73 changes: 10 additions & 63 deletions src/api/form/PDFButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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,
Expand All @@ -12,20 +13,15 @@ import PDFField, {
assertFieldAppearanceOptions,
} from 'src/api/form/PDFField';
import { rgb } from 'src/api/colors';
import {
degrees,
adjustDimsForRotation,
reduceRotation,
} from 'src/api/rotations';
import { drawImage, rotateInPlace } from 'src/api/operations';
import { degrees } from 'src/api/rotations';

import {
PDFRef,
PDFStream,
PDFAcroPushButton,
PDFWidgetAnnotation,
} from 'src/core';
import { assertIs, assertOrUndefined, addRandomSuffix } from 'src/utils';
import { assertIs, assertOrUndefined } from 'src/utils';

/**
* Represents a button field of a [[PDFForm]].
Expand Down Expand Up @@ -71,75 +67,26 @@ export default class PDFButton extends PDFField {
this.acroField = acroPushButton;
}

// NOTE: This 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?
/**
* 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)
* 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) {
// Create appearance stream with image, ignoring caption property
const { context } = this.acroField.dict;

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 rectangle = widget.getRectangle();
const ap = widget.getAppearanceCharacteristics();
const bs = widget.getBorderStyle();

const borderWidth = bs?.getWidth() ?? 1;
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,
const streamRef = this.createImageAppearanceStream(
widget,
image,
alignment,
);

const drawingArea = {
x: 0 + borderWidth,
y: 0 + borderWidth,
width: adj.width - borderWidth * 2,
height: adj.height - borderWidth * 2,
};

// Support borders on images and maybe other properties
const options = {
x: drawingArea.x + (drawingArea.width / 2 - imageDims.width / 2),
y: drawingArea.y + (drawingArea.height / 2 - imageDims.height / 2),
width: imageDims.width,
height: imageDims.height,
//
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
};

const imageName = 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]),
});
const streamRef = context.register(stream);

this.updateWidgetAppearances(widget, { normal: streamRef });
}

Expand Down
89 changes: 87 additions & 2 deletions src/api/form/PDFField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ 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 } from 'src/api/rotations';
import {
Rotation,
toDegrees,
rotateRectangle,
reduceRotation,
adjustDimsForRotation,
degrees,
} from 'src/api/rotations';

import {
PDFRef,
Expand All @@ -15,7 +22,15 @@ import {
PDFAcroTerminal,
AnnotationFlags,
} from 'src/core';
import { assertIs, assertMultiple, assertOrUndefined } from 'src/utils';
import {
addRandomSuffix,
assertIs,
assertMultiple,
assertOrUndefined,
} from 'src/utils';
import { ImageAlignment } from '../image';
import PDFImage from '../PDFImage';
import { drawImage, rotateInPlace } from '../operations';

export interface FieldAppearanceOptions {
x?: number;
Expand Down Expand Up @@ -415,6 +430,76 @@ export default class PDFField {
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 = 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[] },
Expand Down
35 changes: 35 additions & 0 deletions src/api/form/PDFTextField.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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,
Expand All @@ -17,6 +18,7 @@ import {
ExceededMaxLengthError,
InvalidMaxLengthError,
} from 'src/api/errors';
import { ImageAlignment } from 'src/api/image/alignment';
import { TextAlignment } from 'src/api/text/alignment';

import {
Expand Down Expand Up @@ -681,6 +683,39 @@ export default class PDFTextField extends PDFField {
page.node.addAnnot(widgetRef);
}

/**
* 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();
}

/**
* 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:
Expand Down
5 changes: 5 additions & 0 deletions src/api/image/alignment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum ImageAlignment {
Left = 0,
Center = 1,
Right = 2,
}
1 change: 1 addition & 0 deletions src/api/image/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from 'src/api/image/alignment';
2 changes: 1 addition & 1 deletion src/api/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
endPath,
appendBezierCurve,
} from 'src/api/operators';
import { Rotation, toRadians, degrees } from 'src/api/rotations';
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';
Expand Down

0 comments on commit 94f48b0

Please sign in to comment.