Hopding / pdf-lib

Create and modify PDF documents in any JavaScript environment
https://pdf-lib.js.org
MIT License
6.78k stars 647 forks source link

Acrofields are not filled using Nest-js and typescript #425

Closed snakerv closed 4 years ago

snakerv commented 4 years ago

Hi, i have tried to use nestjs in order to make a service that fills acrofields the way i've seen it in this issue : https://github.com/Hopding/pdf-lib/issues/362. But, the problem is that i get the pdf but the fields are not filled at all. But they should since i am using pretty much the same code. Here is the pdf i got : https://www.pdfhost.net/index.php?Action=Download&File=5e3ca19d8d36dd95925c325af149b212

and here is the code of my service :

import { Injectable } from '@nestjs/common';

import * as fs from "fs";
import fetch from 'node-fetch';
import {
  asPDFName,
  degrees,
  drawImage,
  drawText,
  PDFArray,
  PDFContentStream,
  PDFDict,
  PDFDocument,
  PDFFont,
  PDFHexString,
  PDFImage,
  PDFName,
  PDFNumber,
  PDFOperator,
  PDFOperatorNames as Ops,
  popGraphicsState,
  pushGraphicsState,
  rgb,
  rotateDegrees,
  StandardFonts,
  translate,
} from 'pdf-lib';

@Injectable()
export class AppService {

generatepdf(){
  const getAcroForm = (pdfDoc: PDFDocument) =>
  pdfDoc.catalog.lookupMaybe(PDFName.of('AcroForm'), PDFDict);

const getAcroFields = (pdfDoc: PDFDocument): PDFDict[] => {
  const acroForm = getAcroForm(pdfDoc);
  if (!acroForm) return [];

  const fieldRefs = acroForm.lookupMaybe(PDFName.of('Fields'), PDFArray);
  if (!fieldRefs) return [];

  const fields = new Array(fieldRefs.size());
  for (let idx = 0, len = fieldRefs.size(); idx < len; idx++) {
    fields[idx] = fieldRefs.lookup(idx);
  }

  return fields;
};

const findAcroFieldByName = (pdfDoc: PDFDocument, name: string) => {
  const acroFields = getAcroFields(pdfDoc);
  return acroFields.find((acroField) => {
    const fieldName = acroField.get(PDFName.of('T'));
    return fieldName instanceof PDFName && fieldName.value() === name;
  });
};

const imageAppearanceStream = (
  image: PDFImage,
  rotation: number,
  width: number,
  height: number,
) => {
  const dict = image.doc.context.obj({
    Type: 'XObject',
    Subtype: 'Form',
    FormType: 1,
    BBox: [0, 0, width, height],
    Resources: { XObject: { Image: image.ref } },
  });

  const operators = [
    rotateDegrees(rotation),
    translate(0, rotation % 90 === 0 ? -width : 0),
    ...drawImage('Image', {
      x: 0,
      y: 0,
      width: height,
      height: width,
      rotate: degrees(0),
      xSkew: degrees(0),
      ySkew: degrees(0),
    }),
  ];

  const stream = PDFContentStream.of(dict, operators, false);

  return image.doc.context.register(stream);
};

const fillAcroTextField = (acroField: PDFDict, text: string, font: PDFFont) => {
  const rect = acroField.lookup(PDFName.of('Rect'), PDFArray);
  const width =
    rect.lookup(2, PDFNumber).value() - rect.lookup(0, PDFNumber).value();
  const height =
    rect.lookup(3, PDFNumber).value() - rect.lookup(1, PDFNumber).value();

  const MK = acroField.lookupMaybe(PDFName.of('MK'), PDFDict);
  const R = MK && MK.lookupMaybe(PDFName.of('R'), PDFNumber);
  const rotation = R ? R.value() : 0;

  const N = singleLineAppearanceStream(font, text, rotation, width, height);

  acroField.set(PDFName.of('AP'), acroField.context.obj({ N }));
  acroField.set(PDFName.of('Ff'), PDFNumber.of(1 /* Read Only */));
  acroField.set(PDFName.of('V'), PDFHexString.fromText(text));
};

const beginMarkedContent = (tag: string) =>
  PDFOperator.of(Ops.BeginMarkedContent, [asPDFName(tag)]);

const endMarkedContent = () => PDFOperator.of(Ops.EndMarkedContent);

const singleLineAppearanceStream = (
  font: PDFFont,
  text: string,
  rotation: number,
  width: number,
  height: number,
) => {
  const rotationCorrectedHeight = rotation % 90 === 0 ? width : height;

  const size = font.sizeAtHeight(rotationCorrectedHeight - 8);
  const encodedText = font.encodeText(text);
  const x = 0;
  const y = rotationCorrectedHeight - size;

  return textFieldAppearanceStream(
    font,
    size,
    encodedText,
    rotation,
    x,
    y,
    width,
    height,
  );
};

const textFieldAppearanceStream = (
  font: PDFFont,
  size: number,
  encodedText: PDFHexString,
  rotation: number,
  x: number,
  y: number,
  width: number,
  height: number,
) => {
  const dict = font.doc.context.obj({
    Type: 'XObject',
    Subtype: 'Form',
    FormType: 1,
    BBox: [0, 0, width, height],
    Resources: { Font: { F0: font.ref } },
  });

  const operators = [
    rotateDegrees(rotation),
    translate(0, rotation % 90 === 0 ? -width : 0),
    beginMarkedContent('Tx'),
    pushGraphicsState(),
    ...drawText(encodedText, {
      color: rgb(0, 0, 0),
      font: 'F0',
      size,
      rotate: degrees(0),
      xSkew: degrees(0),
      ySkew: degrees(0),
      x,
      y,
    }),
    popGraphicsState(),
    endMarkedContent(),
  ];

  const stream = PDFContentStream.of(dict, operators);

  return font.doc.context.register(stream);
};

(async () => {
  const ticketTemplateBytes = await fetch(
    'https://vivere.s3.amazonaws.com/b2936be1256f34a05dcfe14541ea0ab02f82607ab8282f149d4a157dddaad2da/e2dc9810897d9d40260d25548e5b3633d29d45fa80baedd136800d3a2bee1e84.pdf',
  ).then((res) => res.arrayBuffer());

  const pdfDoc = await PDFDocument.load(ticketTemplateBytes);

  // Fill Form ---------------------------------------------

  const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

  const fillInField = (fieldName: string, text: string) => {
    const field = findAcroFieldByName(pdfDoc, fieldName);
    if (field) fillAcroTextField(field, text, helveticaFont);
  };

  const lockField = (acroField: any) => {
    const fieldType = acroField.lookup(PDFName.of('FT'));
    if (fieldType === PDFName.of('Tx')) {
      acroField.set(PDFName.of('Ff'), PDFNumber.of(1 << 0 /* Read Only */));
    }
  };

  fillInField('STARTDATE', '03/01/2020');
  fillInField('STARTTIME', '1:18 PM');
  fillInField('ADDRESS', '123 Yolk Drive');
  fillInField('FULLNAME', 'Humpty Dumpty');
  fillInField('SERIALNO', '876-ABC-5');
  fillInField('PRICE', '$2500');

  const acroFields = getAcroFields(pdfDoc);
  acroFields.forEach((field) => lockField(field));

  const pdfBytes = await pdfDoc.save();

  fs.writeFileSync('bmr.pdf', pdfBytes);
})();
}

}

and the controller :

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get("pdf")
  getHello() {
    return this.appService.generatepdf();
  }
}

Basically, I don't understand why it doesn't work and fill the fields. If someone can tell me why or what's wrong in my code (but to me, it seems perfectly valid), it will be great. Thank you.

msvargas commented 4 years ago

i have the same issue, try disable font and custom appeareance:

...
const fillAcroTextField = (acroField: PDFDict, text: string, font?: PDFFont) => {
    acroField.set(PDFName.of('V'), PDFString.of(text));
    acroField.delete(PDFName.of('AP'));
 }

...

NOTE: i testing de example in #205 and working properly with custom font!

snakerv commented 4 years ago

i have the same issue, try disable font and custom appeareance:

...
const fillAcroTextField = (acroField: PDFDict, text: string, font?: PDFFont) => {
    acroField.set(PDFName.of('V'), PDFString.of(text));
    acroField.delete(PDFName.of('AP'));
 }

...

NOTE: i testing de example in #205 and working properly with custom font!

thanks. I have try this in the service, but still does not work. Have you got an idea only in order to fill the field (i don't care about styling)? Thank you.

import { Injectable } from '@nestjs/common';

import * as fs from "fs";
import fetch from 'node-fetch';
import {
  asPDFName,
  degrees,
  drawImage,
  drawText,
  PDFArray,
  PDFContentStream,
  PDFDict,
  PDFDocument,
  PDFFont,
  PDFHexString,
  PDFImage,
  PDFName,
  PDFNumber,
  PDFOperator,
  PDFOperatorNames as Ops,
  popGraphicsState,
  pushGraphicsState,
  rgb,
  rotateDegrees,
  StandardFonts,
  translate,
  PDFString,
} from 'pdf-lib';

@Injectable()
export class AppService {

generatepdf(){
  const getAcroForm = (pdfDoc: PDFDocument) =>
  pdfDoc.catalog.lookupMaybe(PDFName.of('AcroForm'), PDFDict);

const getAcroFields = (pdfDoc: PDFDocument): PDFDict[] => {
  const acroForm = getAcroForm(pdfDoc);
  if (!acroForm) return [];

  const fieldRefs = acroForm.lookupMaybe(PDFName.of('Fields'), PDFArray);
  if (!fieldRefs) return [];

  const fields = new Array(fieldRefs.size());
  for (let idx = 0, len = fieldRefs.size(); idx < len; idx++) {
    fields[idx] = fieldRefs.lookup(idx);
  }

  return fields;
};

const findAcroFieldByName = (pdfDoc: PDFDocument, name: string) => {
  const acroFields = getAcroFields(pdfDoc);
  return acroFields.find((acroField) => {
    const fieldName = acroField.get(PDFName.of('T'));
    return fieldName instanceof PDFName && fieldName.value() === name;
  });
};

// const imageAppearanceStream = (
//   image: PDFImage,
//   rotation: number,
//   width: number,
//   height: number,
// ) => {
//   const dict = image.doc.context.obj({
//     Type: 'XObject',
//     Subtype: 'Form',
//     FormType: 1,
//     BBox: [0, 0, width, height],
//     Resources: { XObject: { Image: image.ref } },
//   });

//   const operators = [
//     rotateDegrees(rotation),
//     translate(0, rotation % 90 === 0 ? -width : 0),
//     ...drawImage('Image', {
//       x: 0,
//       y: 0,
//       width: height,
//       height: width,
//       rotate: degrees(0),
//       xSkew: degrees(0),
//       ySkew: degrees(0),
//     }),
//   ];

//   const stream = PDFContentStream.of(dict, operators, false);

//   return image.doc.context.register(stream);
// };

// const fillAcroTextField = (acroField: PDFDict, text: string, font: PDFFont) => {
//   const rect = acroField.lookup(PDFName.of('Rect'), PDFArray);
//   const width =
//     rect.lookup(2, PDFNumber).value() - rect.lookup(0, PDFNumber).value();
//   const height =
//     rect.lookup(3, PDFNumber).value() - rect.lookup(1, PDFNumber).value();

//   const MK = acroField.lookupMaybe(PDFName.of('MK'), PDFDict);
//   const R = MK && MK.lookupMaybe(PDFName.of('R'), PDFNumber);
//   const rotation = R ? R.value() : 0;

//   const N = singleLineAppearanceStream(font, text, rotation, width, height);

//   acroField.set(PDFName.of('AP'), acroField.context.obj({ N }));
//   acroField.set(PDFName.of('Ff'), PDFNumber.of(1 /* Read Only */));
//   acroField.set(PDFName.of('V'), PDFHexString.fromText(text));
// };

const fillAcroTextField = (acroField: PDFDict, text: string, font?: PDFFont) => {
  acroField.set(PDFName.of('V'), PDFString.of(text));
  acroField.delete(PDFName.of('AP'));
}

const beginMarkedContent = (tag: string) =>
  PDFOperator.of(Ops.BeginMarkedContent, [asPDFName(tag)]);

const endMarkedContent = () => PDFOperator.of(Ops.EndMarkedContent);

const singleLineAppearanceStream = (
  font: PDFFont,
  text: string,
  rotation: number,
  width: number,
  height: number,
) => {
  const rotationCorrectedHeight = rotation % 90 === 0 ? width : height;

  const size = font.sizeAtHeight(rotationCorrectedHeight - 8);
  const encodedText = font.encodeText(text);
  const x = 0;
  const y = rotationCorrectedHeight - size;

  return textFieldAppearanceStream(
    font,
    size,
    encodedText,
    rotation,
    x,
    y,
    width,
    height,
  );
};

const textFieldAppearanceStream = (
  font: PDFFont,
  size: number,
  encodedText: PDFHexString,
  rotation: number,
  x: number,
  y: number,
  width: number,
  height: number,
) => {
  const dict = font.doc.context.obj({
    Type: 'XObject',
    Subtype: 'Form',
    FormType: 1,
    BBox: [0, 0, width, height],
    Resources: { Font: { F0: font.ref } },
  });

  const operators = [
    rotateDegrees(rotation),
    translate(0, rotation % 90 === 0 ? -width : 0),
    beginMarkedContent('Tx'),
    pushGraphicsState(),
    ...drawText(encodedText, {
      color: rgb(0, 0, 0),
      font: 'F0',
      size,
      rotate: degrees(0),
      xSkew: degrees(0),
      ySkew: degrees(0),
      x,
      y,
    }),
    popGraphicsState(),
    endMarkedContent(),
  ];

  const stream = PDFContentStream.of(dict, operators);

  return font.doc.context.register(stream);
};

(async () => {
  const ticketTemplateBytes = await fetch(
    'https://vivere.s3.amazonaws.com/b2936be1256f34a05dcfe14541ea0ab02f82607ab8282f149d4a157dddaad2da/e2dc9810897d9d40260d25548e5b3633d29d45fa80baedd136800d3a2bee1e84.pdf',
  ).then((res) => res.arrayBuffer());

  const pdfDoc = await PDFDocument.load(ticketTemplateBytes);

  // Fill Form ---------------------------------------------

  const timesFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);

  const fillInField = (fieldName: string, text: string) => {
    const field = findAcroFieldByName(pdfDoc, fieldName);
    if (field) fillAcroTextField(field, text, timesFont);
  };

  // const lockField = (acroField: any) => {
  //   const fieldType = acroField.lookup(PDFName.of('FT'));
  //   if (fieldType === PDFName.of('Tx')) {
  //     acroField.set(PDFName.of('Ff'), PDFNumber.of(1 << 0 /* Read Only */));
  //   }
  // };

  fillInField('STARTDATE', '03/01/2020');
  fillInField('STARTTIME', '1:18 PM');
  fillInField('ADDRESS', '123 Yolk Drive');
  fillInField('FULLNAME', 'Humpty Dumpty');
  fillInField('SERIALNO', '876-ABC-5');
  fillInField('PRICE', '$2500');

  //const acroFields = 
  getAcroFields(pdfDoc);
  //acroFields.forEach((field) => lockField(field));

  const pdfBytes = await pdfDoc.save();

  fs.writeFileSync('nope.pdf', pdfBytes);
})();
}

}

nope.pdf

msvargas commented 4 years ago

Sorry i forgot you need enable NeedAppearances, maybe your original code working properly:

const acroForm = getAcroForm(pdfDoc);
acroForm.set(PDFName.of('NeedAppearances'), PDFBool.True)
Hopding commented 4 years ago

Hello @snakerv!

I've provided an updated version of the findAcroFieldByName function in the example you are referencing: https://github.com/Hopding/pdf-lib/issues/362#issuecomment-620884706. If you use this new version of the function it should fix your issue.

I hope this helps!

snakerv commented 4 years ago

Thank you guys, working perfectly fine thanks to you both. Thanks again for the help! :)

Hopding commented 4 years ago

pdf-lib now has form creation and filling APIs that should be used instead of the above example(s). See the form filling JSFiddle for a working example. Additional information is available in the README and API docs. The PDFTextField.setText method is of particular relevance to this issue.