diff --git a/src/api/form/PDFField.ts b/src/api/form/PDFField.ts index 259a0910f..553c488d8 100644 --- a/src/api/form/PDFField.ts +++ b/src/api/form/PDFField.ts @@ -11,8 +11,9 @@ import { PDFName, PDFDict, MethodNotImplementedError, - PDFAcroField, AcroFieldFlags, + PDFAcroTerminal, + AnnotationFlags, } from 'src/core'; import { assertIs, assertMultiple, assertOrUndefined } from 'src/utils'; @@ -70,8 +71,8 @@ export const assertFieldAppearanceOptions = ( * to be rendered. */ export default class PDFField { - /** The low-level PDFAcroField wrapped by this field. */ - readonly acroField: PDFAcroField; + /** The low-level PDFAcroTerminal wrapped by this field. */ + readonly acroField: PDFAcroTerminal; /** The unique reference assigned to this field within the document. */ readonly ref: PDFRef; @@ -80,11 +81,11 @@ export default class PDFField { readonly doc: PDFDocument; protected constructor( - acroField: PDFAcroField, + acroField: PDFAcroTerminal, ref: PDFRef, doc: PDFDocument, ) { - assertIs(acroField, 'acroField', [[PDFAcroField, 'PDFAcroField']]); + assertIs(acroField, 'acroField', [[PDFAcroTerminal, 'PDFAcroTerminal']]); assertIs(ref, 'ref', [[PDFRef, 'PDFRef']]); assertIs(doc, 'doc', [[PDFDocument, 'PDFDocument']]); @@ -311,6 +312,10 @@ export default class PDFField { const bs = widget.getOrCreateBorderStyle(); if (borderWidth !== undefined) bs.setWidth(borderWidth); + widget.setFlagTo(AnnotationFlags.Print, true); + widget.setFlagTo(AnnotationFlags.Hidden, false); + widget.setFlagTo(AnnotationFlags.Invisible, false); + // Set acrofield properties if (textColor) { const da = this.acroField.getDefaultAppearance() ?? ''; diff --git a/src/core/annotation/PDFAnnotation.ts b/src/core/annotation/PDFAnnotation.ts index 7953f39f1..135016748 100644 --- a/src/core/annotation/PDFAnnotation.ts +++ b/src/core/annotation/PDFAnnotation.ts @@ -3,6 +3,7 @@ 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; @@ -22,6 +23,11 @@ class PDFAnnotation { 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 }; @@ -95,6 +101,34 @@ class PDFAnnotation { 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; diff --git a/src/core/annotation/flags.ts b/src/core/annotation/flags.ts new file mode 100644 index 000000000..9730ecd4b --- /dev/null +++ b/src/core/annotation/flags.ts @@ -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 annotation’s 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 annotation’s 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 annotation’s 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), +} diff --git a/src/core/annotation/index.ts b/src/core/annotation/index.ts index a0ac4e745..60b9d6d57 100644 --- a/src/core/annotation/index.ts +++ b/src/core/annotation/index.ts @@ -1,3 +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'; diff --git a/tests/api/form/PDFCheckBox.spec.ts b/tests/api/form/PDFCheckBox.spec.ts index 9b4252396..743cf1904 100644 --- a/tests/api/form/PDFCheckBox.spec.ts +++ b/tests/api/form/PDFCheckBox.spec.ts @@ -1,5 +1,5 @@ import fs from 'fs'; -import { PDFDocument } from 'src/index'; +import { PDFDocument, AnnotationFlags } from 'src/index'; const fancyFieldsPdfBytes = fs.readFileSync('assets/pdfs/fancy_fields.pdf'); const pdfDocPromise = PDFDocument.load(fancyFieldsPdfBytes); @@ -36,4 +36,20 @@ describe(`PDFCheckBox`, () => { expect(isAFairy.isReadOnly()).toBe(false); expect(isAFairy.isRequired()).toBe(false); }); + + it(`produces printable widgets when added to a page`, async () => { + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage(); + + const form = pdfDoc.getForm(); + + const checkBox = form.createCheckBox('a.new.check.box'); + + const widgets = () => checkBox.acroField.getWidgets(); + expect(widgets().length).toBe(0); + + checkBox.addToPage(page); + expect(widgets().length).toBe(1); + expect(widgets()[0].hasFlag(AnnotationFlags.Print)).toBe(true); + }); }); diff --git a/tests/api/form/PDFDropdown.spec.ts b/tests/api/form/PDFDropdown.spec.ts index e54253dbe..9bce33891 100644 --- a/tests/api/form/PDFDropdown.spec.ts +++ b/tests/api/form/PDFDropdown.spec.ts @@ -1,5 +1,5 @@ import fs from 'fs'; -import { PDFDocument } from 'src/index'; +import { PDFDocument, AnnotationFlags } from 'src/index'; const fancyFieldsPdfBytes = fs.readFileSync('assets/pdfs/fancy_fields.pdf'); @@ -83,4 +83,20 @@ describe(`PDFDropdown`, () => { expect(gundams.isSorted()).toBe(false); expect(gundams.isSpellChecked()).toBe(true); }); + + it(`produces printable widgets when added to a page`, async () => { + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage(); + + const form = pdfDoc.getForm(); + + const dropdown = form.createDropdown('a.new.dropdown'); + + const widgets = () => dropdown.acroField.getWidgets(); + expect(widgets().length).toBe(0); + + dropdown.addToPage(page); + expect(widgets().length).toBe(1); + expect(widgets()[0].hasFlag(AnnotationFlags.Print)).toBe(true); + }); }); diff --git a/tests/api/form/PDFOptionList.spec.ts b/tests/api/form/PDFOptionList.spec.ts index 1bc72ee29..dd769874c 100644 --- a/tests/api/form/PDFOptionList.spec.ts +++ b/tests/api/form/PDFOptionList.spec.ts @@ -1,5 +1,5 @@ import fs from 'fs'; -import { PDFDocument } from 'src/index'; +import { PDFDocument, AnnotationFlags } from 'src/index'; const fancyFieldsPdfBytes = fs.readFileSync('assets/pdfs/fancy_fields.pdf'); @@ -69,4 +69,20 @@ describe(`PDFOptionList`, () => { expect(planets.isSelectOnClick()).toBe(false); expect(planets.isSorted()).toBe(false); }); + + it(`produces printable widgets when added to a page`, async () => { + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage(); + + const form = pdfDoc.getForm(); + + const optionList = form.createOptionList('a.new.option.list'); + + const widgets = () => optionList.acroField.getWidgets(); + expect(widgets().length).toBe(0); + + optionList.addToPage(page); + expect(widgets().length).toBe(1); + expect(widgets()[0].hasFlag(AnnotationFlags.Print)).toBe(true); + }); }); diff --git a/tests/api/form/PDFRadioGroup.spec.ts b/tests/api/form/PDFRadioGroup.spec.ts index b421f9d6f..392dc1d0f 100644 --- a/tests/api/form/PDFRadioGroup.spec.ts +++ b/tests/api/form/PDFRadioGroup.spec.ts @@ -1,5 +1,11 @@ import fs from 'fs'; -import { PDFDocument, PDFName, PDFArray, PDFHexString } from 'src/index'; +import { + PDFDocument, + PDFName, + PDFArray, + PDFHexString, + AnnotationFlags, +} from 'src/index'; const fancyFieldsPdfBytes = fs.readFileSync('assets/pdfs/fancy_fields.pdf'); @@ -142,4 +148,20 @@ describe(`PDFRadioGroup`, () => { expect((opt.get(2) as PDFHexString).decodeText()).toBe('foo'); expect((opt.get(3) as PDFHexString).decodeText()).toBe('qux'); }); + + it(`produces printable widgets when added to a page`, async () => { + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage(); + + const form = pdfDoc.getForm(); + + const radioGroup = form.createRadioGroup('a.new.radio.group'); + + const widgets = () => radioGroup.acroField.getWidgets(); + expect(widgets().length).toBe(0); + + radioGroup.addOptionToPage('foo', page); + expect(widgets().length).toBe(1); + expect(widgets()[0].hasFlag(AnnotationFlags.Print)).toBe(true); + }); }); diff --git a/tests/api/form/PDFTextField.spec.ts b/tests/api/form/PDFTextField.spec.ts index 0923a0dad..60fcb31ea 100644 --- a/tests/api/form/PDFTextField.spec.ts +++ b/tests/api/form/PDFTextField.spec.ts @@ -1,5 +1,5 @@ import fs from 'fs'; -import { PDFDocument, TextAlignment } from 'src/index'; +import { PDFDocument, TextAlignment, AnnotationFlags } from 'src/index'; const fancyFieldsPdfBytes = fs.readFileSync('assets/pdfs/fancy_fields.pdf'); @@ -95,4 +95,20 @@ describe(`PDFTextField`, () => { expect(() => textField.setMaxLength(7)).not.toThrow(); expect(() => textField.setMaxLength(5)).toThrow(); }); + + it(`produces printable widgets when added to a page`, async () => { + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage(); + + const form = pdfDoc.getForm(); + + const textField = form.createTextField('a.new.text.field'); + + const widgets = () => textField.acroField.getWidgets(); + expect(widgets().length).toBe(0); + + textField.addToPage(page); + expect(widgets().length).toBe(1); + expect(widgets()[0].hasFlag(AnnotationFlags.Print)).toBe(true); + }); });