Skip to content

Commit

Permalink
Make new widgets printable (#604)
Browse files Browse the repository at this point in the history
* Make new widgets printable; Fix AcroField type error

* Test that new widgets are printable

* Reset scratchpad
  • Loading branch information
Hopding authored Sep 15, 2020
1 parent 95adc0f commit dfe7c47
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 10 deletions.
15 changes: 10 additions & 5 deletions src/api/form/PDFField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
PDFName,
PDFDict,
MethodNotImplementedError,
PDFAcroField,
AcroFieldFlags,
PDFAcroTerminal,
AnnotationFlags,
} from 'src/core';
import { assertIs, assertMultiple, assertOrUndefined } from 'src/utils';

Expand Down Expand Up @@ -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;
Expand All @@ -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']]);

Expand Down Expand Up @@ -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() ?? '';
Expand Down
34 changes: 34 additions & 0 deletions src/core/annotation/PDFAnnotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 };
Expand Down Expand Up @@ -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;
90 changes: 90 additions & 0 deletions src/core/annotation/flags.ts
Original file line number Diff line number Diff line change
@@ -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),
}
1 change: 1 addition & 0 deletions src/core/annotation/index.ts
Original file line number Diff line number Diff line change
@@ -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';
18 changes: 17 additions & 1 deletion tests/api/form/PDFCheckBox.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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);
});
});
18 changes: 17 additions & 1 deletion tests/api/form/PDFDropdown.spec.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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);
});
});
18 changes: 17 additions & 1 deletion tests/api/form/PDFOptionList.spec.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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);
});
});
24 changes: 23 additions & 1 deletion tests/api/form/PDFRadioGroup.spec.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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);
});
});
18 changes: 17 additions & 1 deletion tests/api/form/PDFTextField.spec.ts
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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);
});
});

0 comments on commit dfe7c47

Please sign in to comment.