Skip to content

Commit

Permalink
Form Flatten (#716)
Browse files Browse the repository at this point in the history
* Form Flatten

* Code review
  • Loading branch information
btecu authored Dec 21, 2020
1 parent 79e414c commit 560aec6
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 6 deletions.
4 changes: 3 additions & 1 deletion apps/deno/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { default as test14 } from './tests/test14.ts';
import { default as test15 } from './tests/test15.ts';
import { default as test16 } from './tests/test16.ts';
import { default as test17 } from './tests/test17.ts';
import { default as test18 } from './tests/test18.ts';

const promptToContinue = () => {
const prompt = 'Press <enter> to run the next test...';
Expand Down Expand Up @@ -132,6 +133,7 @@ const assets = {
dod_character: readPdf('dod_character.pdf'),
with_xfa_fields: readPdf('with_xfa_fields.pdf'),
fancy_fields: readPdf('fancy_fields.pdf'),
form_to_flatten: readPdf('form_to_flatten.pdf'),
},
};

Expand Down Expand Up @@ -162,7 +164,7 @@ const main = async () => {
// prettier-ignore
const allTests = [
test1, test2, test3, test4, test5, test6, test7, test8, test9, test10,
test11, test12, test13, test14, test15, test16, test17
test11, test12, test13, test14, test15, test16, test17, test18
];

const tests = testIdx ? [allTests[testIdx - 1]] : allTests;
Expand Down
29 changes: 29 additions & 0 deletions apps/deno/tests/test18.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Assets } from '../index.ts';

// @deno-types="../dummy.d.ts"
import { PDFDocument } from '../../../dist/pdf-lib.esm.js';

export default async (assets: Assets) => {
const pdfDoc = await PDFDocument.load(assets.pdfs.form_to_flatten);

const form = pdfDoc.getForm();

form.getTextField('Text1').setText('Some Text');

form.getRadioGroup('Group2').select('Choice1');
form.getRadioGroup('Group3').select('Choice3');
form.getRadioGroup('Group4').select('Choice1');

form.getCheckBox('Check Box3').check();
form.getCheckBox('Check Box4').uncheck();

form.getDropdown('Dropdown7').select('Infinity');

form.getOptionList('List Box6').select('Honda');

form.flatten();

const pdfBytes = await pdfDoc.save();

return pdfBytes;
};
4 changes: 3 additions & 1 deletion apps/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import test14 from './tests/test14';
import test15 from './tests/test15';
import test16 from './tests/test16';
import test17 from './tests/test17';
import test18 from './tests/test18';

const cli = readline.createInterface({
input: process.stdin,
Expand Down Expand Up @@ -130,6 +131,7 @@ const assets = {
dod_character: readPdf('dod_character.pdf'),
with_xfa_fields: readPdf('with_xfa_fields.pdf'),
fancy_fields: readPdf('fancy_fields.pdf'),
form_to_flatten: readPdf('form_to_flatten.pdf'),
},
};

Expand Down Expand Up @@ -160,7 +162,7 @@ const main = async () => {
// prettier-ignore
const allTests = [
test1, test2, test3, test4, test5, test6, test7, test8, test9, test10,
test11, test12, test13, test14, test15, test16, test17
test11, test12, test13, test14, test15, test16, test17, test18
];

const tests = testIdx ? [allTests[testIdx - 1]] : allTests;
Expand Down
27 changes: 27 additions & 0 deletions apps/node/tests/test18.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Assets } from '..';
import { PDFDocument } from '../../..';

export default async (assets: Assets) => {
const pdfDoc = await PDFDocument.load(assets.pdfs.form_to_flatten);

const form = pdfDoc.getForm();

form.getTextField('Text1').setText('Some Text');

form.getRadioGroup('Group2').select('Choice1');
form.getRadioGroup('Group3').select('Choice3');
form.getRadioGroup('Group4').select('Choice1');

form.getCheckBox('Check Box3').check();
form.getCheckBox('Check Box4').uncheck();

form.getDropdown('Dropdown7').select('Infinity');

form.getOptionList('List Box6').select('Honda');

form.flatten();

const pdfBytes = await pdfDoc.save();

return pdfBytes;
};
2 changes: 2 additions & 0 deletions apps/rn/src/components/TestLauncher.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import test14 from '../tests/test14';
import test15 from '../tests/test15';
import test16 from '../tests/test16';
import test17 from '../tests/test17';
import test18 from '../tests/test18';

const red = '#FF0000';

Expand Down Expand Up @@ -91,6 +92,7 @@ export default class TestLauncher extends Component {
<TestButton test={[15, test15]} longRunning />
<TestButton test={[16, test16]} />
<TestButton test={[17, test17]} />
<TestButton test={[18, test18]} />
</ScrollView>
</SafeAreaView>
);
Expand Down
30 changes: 30 additions & 0 deletions apps/rn/src/tests/test18.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { PDFDocument } from 'pdf-lib';

import { fetchAsset } from './assets';

export default async () => {
const formToFlattenPdf = await fetchAsset('pdfs/form_to_flatten.pdf');

const pdfDoc = await PDFDocument.load(formToFlattenPdf);

const form = pdfDoc.getForm();

form.getTextField('Text1').setText('Some Text');

form.getRadioGroup('Group2').select('Choice1');
form.getRadioGroup('Group3').select('Choice3');
form.getRadioGroup('Group4').select('Choice1');

form.getCheckBox('Check Box3').check();
form.getCheckBox('Check Box4').uncheck();

form.getDropdown('Dropdown7').select('Infinity');

form.getOptionList('List Box6').select('Honda');

form.flatten();

const base64Pdf = await pdfDoc.saveAsBase64({ dataUri: true });

return { base64Pdf };
};
2 changes: 1 addition & 1 deletion apps/web/test17.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
Prev
</button>
<button onclick="test()">Run Test</button>
<button disabled onclick="window.location.href = '/apps/web/test18.html'">
<button onclick="window.location.href = '/apps/web/test18.html'">
Next
</button>
</div>
Expand Down
78 changes: 78 additions & 0 deletions apps/web/test18.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self' 'unsafe-inline' blob: resource:;
object-src 'self' blob:;
frame-src 'self' blob:;
"
/>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" type="text/css" href="/apps/web/index.css" />
<title>Test 18</title>
<script type="text/javascript" src="/dist/pdf-lib.js"></script>
<script type="text/javascript" src="/apps/web/utils.js"></script>
</head>

<body>
<div id="button-container">
<button onclick="window.location.href = '/apps/web/test17.html'">
Prev
</button>
<button onclick="test()">Run Test</button>
<button disabled onclick="window.location.href = '/apps/web/test19.html'">
Next
</button>
</div>
<div id="animation-target"></div>
<iframe id="iframe"></iframe>
</body>

<script type="text/javascript">
startFpsTracker('animation-target');

const fetchBinaryAsset = (asset) =>
fetch(`/assets/${asset}`).then((res) => res.arrayBuffer());

const renderInIframe = (pdfBytes) => {
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
document.getElementById('iframe').src = blobUrl;
};

async function test() {
const { PDFDocument } = PDFLib;

const formToFlattenPdf = await fetchBinaryAsset(
'pdfs/form_to_flatten.pdf',
);

const pdfDoc = await PDFDocument.load(formToFlattenPdf);

const form = pdfDoc.getForm();

form.getTextField('Text1').setText('Some Text');

form.getRadioGroup('Group2').select('Choice1');
form.getRadioGroup('Group3').select('Choice3');
form.getRadioGroup('Group4').select('Choice1');

form.getCheckBox('Check Box3').check();
form.getCheckBox('Check Box4').uncheck();

form.getDropdown('Dropdown7').select('Infinity');

form.getOptionList('List Box6').select('Honda');

form.flatten();

const pdfBytes = await pdfDoc.save();

renderInIframe(pdfBytes);
}
</script>
</html>
Binary file added assets/pdfs/form_to_flatten.pdf
Binary file not shown.
92 changes: 90 additions & 2 deletions src/api/form/PDFForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ import {
} from 'src/api/errors';
import PDFFont from 'src/api/PDFFont';
import { StandardFonts } from 'src/api/StandardFonts';

import {
drawObject,
popGraphicsState,
pushGraphicsState,
rotateRadians,
translate,
} from 'src/api/operators';
import { degrees, toRadians } from 'src/api/rotations';
import {
PDFAcroForm,
PDFAcroField,
Expand All @@ -27,11 +34,17 @@ import {
PDFAcroText,
PDFAcroPushButton,
PDFAcroNonTerminal,
PDFDict,
PDFOperator,
PDFRef,
createPDFAcroFields,
PDFName,
} from 'src/core';
import { assertIs, Cache, assertOrUndefined } from 'src/utils';
import { addRandomSuffix, assertIs, Cache, assertOrUndefined } from 'src/utils';

export interface FlattenOptions {
updateFieldAppearances: boolean;
}

/**
* Represents the interactive form of a [[PDFDocument]].
Expand Down Expand Up @@ -499,6 +512,81 @@ export default class PDFForm {
return PDFTextField.of(text, text.ref, this.doc);
}

/**
* Flatten all form fields.
*
* Flattening a form field will take the current appearance and make that part
* of the pages content stream. All form fields and annotations associated are removed.
*
* For example:
* ```js
* const form = pdfDoc.getForm();
* form.flatten();
* ```
*/
flatten(options: FlattenOptions = { updateFieldAppearances: true }) {
if (options.updateFieldAppearances) {
this.updateFieldAppearances();
}

const fields = this.getFields();
const pages = this.doc.getPages();

for (let i = 0, lenFields = fields.length; i < lenFields; i++) {
const field = fields[i];
const widgets = field.acroField.getWidgets();

for (let j = 0, lenWidgets = widgets.length; j < lenWidgets; j++) {
const widget = widgets[j];
const pageRef = widget.P();
const page = pages.find((x) => x.ref === pageRef);
if (page === undefined) {
throw new Error(
`Failed to find page ${pageRef} for element ${field.getName()}`,
);
}

let refOrDict = widget.getNormalAppearance();

if (
refOrDict instanceof PDFDict &&
(field instanceof PDFCheckBox || field instanceof PDFRadioGroup)
) {
const value = field.acroField.getValue();
const ref = refOrDict.get(value) ?? refOrDict.get(PDFName.of('Off'));

if (ref instanceof PDFRef) {
refOrDict = ref;
}
}

if (!(refOrDict instanceof PDFRef)) {
throw new Error(`Failed to extract appearance ref`);
}

const xObjectKey = addRandomSuffix('FlatWidget', 10);
page.node.setXObject(PDFName.of(xObjectKey), refOrDict);

const ap = widget.getAppearanceCharacteristics();
const rectangle = widget.getRectangle();
const rotation = degrees(ap?.getRotation() ?? 0);

const operators = [
pushGraphicsState(),
translate(rectangle.x, rectangle.y),
rotateRadians(toRadians(rotation)),
drawObject(xObjectKey),
popGraphicsState(),
].filter(Boolean) as PDFOperator[];

page.pushOperators(...operators);
}

this.acroForm.removeField(field.ref);
this.doc.context.delete(field.ref);
}
}

/**
* Update the appearance streams for all widgets of all fields in this
* [[PDFForm]]. Appearance streams will only be created for a widget if it
Expand Down
8 changes: 8 additions & 0 deletions src/core/acroform/PDFAcroForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ class PDFAcroForm {
Fields?.push(field);
}

removeField(field: PDFRef) {
const { Fields } = this.normalizedEntries();
const index = Fields?.indexOf(field);
if (index !== undefined) {
Fields.remove(index);
}
}

normalizedEntries() {
let Fields = this.Fields();

Expand Down
8 changes: 8 additions & 0 deletions src/core/annotation/PDFAnnotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ class PDFAnnotation {
return AP;
}

getNormalAppearance(): PDFRef | PDFDict {
const AP = this.ensureAP();
const N = AP.get(PDFName.of('N'));
if (N instanceof PDFRef || N instanceof PDFDict) return N;

throw new Error(`Unexpected N type: ${N?.constructor.name}`);
}

/** @param appearance A PDFDict or PDFStream (direct or ref) */
setNormalAppearance(appearance: PDFRef | PDFDict) {
const AP = this.ensureAP();
Expand Down
2 changes: 1 addition & 1 deletion src/core/annotation/PDFWidgetAnnotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class PDFWidgetAnnotation extends PDFAnnotation {
}

P(): PDFRef | undefined {
const P = this.dict.lookup(PDFName.of('P'));
const P = this.dict.get(PDFName.of('P'));
if (P instanceof PDFRef) return P;
return undefined;
}
Expand Down
Loading

0 comments on commit 560aec6

Please sign in to comment.