mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-23 23:10:17 +05:30
feat(pdf): add form field detection, fill, and flatten
Adds pdfGetFormFields (lists AcroForm fields with name/type/value) and pdfFillForm (fills text fields by name, optionally flattens) to PDFOperations.js, dispatched via 'formFields'/'fillForm' in executeOperation. pdfFillForm skips unknown/non-text fields per-field (logs + continues) rather than failing the whole batch, matching the partial-success precedent set by pdfExtractImages. Wires a "Fill Form" entry into the PDF editor dialog: selecting a PDF fetches its fields via a new get-pdf-form-fields/pdf-form-fields IPC round trip and renders one text input per field, plus a flatten checkbox, following the same structure as the crop/pageNumbers dialogs. Amit Haridas
This commit is contained in:
@@ -230,3 +230,158 @@ describe('PDFOperations - Task 15 new operations', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PDFOperations - Task 16 form field fill/flatten', () => {
|
||||
let tmpDir, plainInputPath;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pdfops_form_'));
|
||||
plainInputPath = path.join(tmpDir, 'plain.pdf');
|
||||
|
||||
const doc = await PDFDocument.create();
|
||||
doc.addPage([600, 800]);
|
||||
fs.writeFileSync(plainInputPath, await doc.save());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Builds a fixture PDF with a real AcroForm text field via pdf-lib's
|
||||
// form.createTextField() API, mirroring pdf-lib's documented form-creation flow.
|
||||
async function buildFormPdf(fileName, initialValue = 'John Doe') {
|
||||
const doc = await PDFDocument.create();
|
||||
const page = doc.addPage([600, 800]);
|
||||
const form = doc.getForm();
|
||||
const nameField = form.createTextField('name');
|
||||
nameField.setText(initialValue);
|
||||
nameField.addToPage(page, { x: 50, y: 700, width: 200, height: 20 });
|
||||
|
||||
const filePath = path.join(tmpDir, fileName);
|
||||
fs.writeFileSync(filePath, await doc.save());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
describe('pdfGetFormFields', () => {
|
||||
it('lists text fields with name, type, and current value', async () => {
|
||||
const formPath = await buildFormPdf('form.pdf', 'John Doe');
|
||||
|
||||
const result = await PDFOperations.pdfGetFormFields({ inputPath: formPath });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.fields).toEqual([{ name: 'name', type: 'PDFTextField', value: 'John Doe' }]);
|
||||
});
|
||||
|
||||
it('returns an empty fields array for a PDF with no AcroForm', async () => {
|
||||
const result = await PDFOperations.pdfGetFormFields({ inputPath: plainInputPath });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.fields).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns failure for a nonexistent file', async () => {
|
||||
const result = await PDFOperations.pdfGetFormFields({
|
||||
inputPath: path.join(tmpDir, 'missing.pdf'),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pdfFillForm', () => {
|
||||
it('fills a text field with the given value', async () => {
|
||||
const formPath = await buildFormPdf('form.pdf', '');
|
||||
const outputPath = path.join(tmpDir, 'filled.pdf');
|
||||
|
||||
const result = await PDFOperations.pdfFillForm({
|
||||
inputPath: formPath,
|
||||
outputPath,
|
||||
values: { name: 'Jane Smith' },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(fs.existsSync(outputPath)).toBe(true);
|
||||
|
||||
const filled = await PDFDocument.load(fs.readFileSync(outputPath));
|
||||
expect(filled.getForm().getTextField('name').getText()).toBe('Jane Smith');
|
||||
});
|
||||
|
||||
it('flattens the form when flatten is true, removing editable fields', async () => {
|
||||
const formPath = await buildFormPdf('form.pdf', '');
|
||||
const outputPath = path.join(tmpDir, 'flattened.pdf');
|
||||
|
||||
const result = await PDFOperations.pdfFillForm({
|
||||
inputPath: formPath,
|
||||
outputPath,
|
||||
values: { name: 'Jane Smith' },
|
||||
flatten: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const flattened = await PDFDocument.load(fs.readFileSync(outputPath));
|
||||
expect(flattened.getForm().getFields().length).toBe(0);
|
||||
});
|
||||
|
||||
it('does not flatten when flatten is false/omitted', async () => {
|
||||
const formPath = await buildFormPdf('form.pdf', '');
|
||||
const outputPath = path.join(tmpDir, 'not-flattened.pdf');
|
||||
|
||||
const result = await PDFOperations.pdfFillForm({
|
||||
inputPath: formPath,
|
||||
outputPath,
|
||||
values: { name: 'Jane Smith' },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const notFlattened = await PDFDocument.load(fs.readFileSync(outputPath));
|
||||
expect(notFlattened.getForm().getFields().length).toBe(1);
|
||||
});
|
||||
|
||||
it('skips a value for a field that does not exist, continuing with the rest', async () => {
|
||||
const formPath = await buildFormPdf('form.pdf', '');
|
||||
const outputPath = path.join(tmpDir, 'filled-partial.pdf');
|
||||
|
||||
const result = await PDFOperations.pdfFillForm({
|
||||
inputPath: formPath,
|
||||
outputPath,
|
||||
values: { name: 'Jane Smith', doesNotExist: 'whatever' },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const filled = await PDFDocument.load(fs.readFileSync(outputPath));
|
||||
expect(filled.getForm().getTextField('name').getText()).toBe('Jane Smith');
|
||||
});
|
||||
|
||||
it('returns failure for a nonexistent input file', async () => {
|
||||
const result = await PDFOperations.pdfFillForm({
|
||||
inputPath: path.join(tmpDir, 'missing.pdf'),
|
||||
outputPath: path.join(tmpDir, 'out.pdf'),
|
||||
values: { name: 'X' },
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('executeOperation dispatch', () => {
|
||||
it('dispatches formFields', async () => {
|
||||
const formPath = await buildFormPdf('form.pdf', 'John Doe');
|
||||
const result = await PDFOperations.executeOperation('formFields', { inputPath: formPath });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.fields.length).toBe(1);
|
||||
});
|
||||
|
||||
it('dispatches fillForm', async () => {
|
||||
const formPath = await buildFormPdf('form.pdf', '');
|
||||
const outputPath = path.join(tmpDir, 'dispatch-filled.pdf');
|
||||
const result = await PDFOperations.executeOperation('fillForm', {
|
||||
inputPath: formPath,
|
||||
outputPath,
|
||||
values: { name: 'Dispatch Test' },
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user