Repository: Hopding/pdf-lib
Pull request #604 (2020-09-15 23:35:43)

Make new widgets printable
Addresses #603


Patch:
diff --git a/src/api/form/PDFField.ts b/src/api/form/PDFField.ts
index 259a091..553c488 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 7953f39..1350167 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 0000000..9730ecd
--- /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 a0ac4e7..60b9d6d 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';


Test patch:
diff --git a/tests/api/form/PDFCheckBox.spec.ts b/tests/api/form/PDFCheckBox.spec.ts
index 9b42523..743cf19 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 e54253d..9bce338 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 1bc72ee..dd76987 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 b421f9d..392dc1d 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 0923a0d..60fcb31 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);
+  });
 });