diff --git a/README.md b/README.md index ef2aa2578..d5cd5518c 100644 --- a/README.md +++ b/README.md @@ -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') diff --git a/apps/deno/tests/test15.ts b/apps/deno/tests/test15.ts index 4a0553a11..da0a26e28 100644 --- a/apps/deno/tests/test15.ts +++ b/apps/deno/tests/test15.ts @@ -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') diff --git a/apps/node/tests/test15.ts b/apps/node/tests/test15.ts index 679040639..b902060c4 100644 --- a/apps/node/tests/test15.ts +++ b/apps/node/tests/test15.ts @@ -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') diff --git a/apps/rn/src/tests/test15.js b/apps/rn/src/tests/test15.js index d1f2da466..5f8c7db34 100644 --- a/apps/rn/src/tests/test15.js +++ b/apps/rn/src/tests/test15.js @@ -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') diff --git a/apps/web/test15.html b/apps/web/test15.html index 6b0882b58..00462c518 100644 --- a/apps/web/test15.html +++ b/apps/web/test15.html @@ -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') diff --git a/assets/pdfs/dod_character.pdf b/assets/pdfs/dod_character.pdf index a59d2f943..f5701d609 100644 Binary files a/assets/pdfs/dod_character.pdf and b/assets/pdfs/dod_character.pdf differ diff --git a/src/api/form/PDFButton.ts b/src/api/form/PDFButton.ts index 91e217e45..acf623ce2 100644 --- a/src/api/form/PDFButton.ts +++ b/src/api/form/PDFButton.ts @@ -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, @@ -12,12 +13,7 @@ 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, @@ -25,7 +21,7 @@ import { 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]]. @@ -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 }); } diff --git a/src/api/form/PDFField.ts b/src/api/form/PDFField.ts index 553c488d8..aa867eeaf 100644 --- a/src/api/form/PDFField.ts +++ b/src/api/form/PDFField.ts @@ -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, @@ -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; @@ -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[] }, diff --git a/src/api/form/PDFTextField.ts b/src/api/form/PDFTextField.ts index 4e475b6fa..6e4424194 100644 --- a/src/api/form/PDFTextField.ts +++ b/src/api/form/PDFTextField.ts @@ -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, @@ -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 { @@ -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: diff --git a/src/api/image/alignment.ts b/src/api/image/alignment.ts new file mode 100644 index 000000000..9fcc1eecc --- /dev/null +++ b/src/api/image/alignment.ts @@ -0,0 +1,5 @@ +export enum ImageAlignment { + Left = 0, + Center = 1, + Right = 2, +} diff --git a/src/api/image/index.ts b/src/api/image/index.ts new file mode 100644 index 000000000..171a63fb8 --- /dev/null +++ b/src/api/image/index.ts @@ -0,0 +1 @@ +export * from 'src/api/image/alignment'; diff --git a/src/api/operations.ts b/src/api/operations.ts index 677e39f21..05178ef8d 100644 --- a/src/api/operations.ts +++ b/src/api/operations.ts @@ -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';