Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support pdf filling with non-english characters #11

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@fortawesome/free-regular-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.6",
"@pdf-lib/fontkit": "0.0.4",
"antd": "^3.11.6",
"axios": "^0.18.0",
"bcrypt": "^3.0.4",
Expand All @@ -39,7 +40,7 @@
"nodemailer": "^5.1.1",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"pdf-fill-form": "^5.0.0",
"pdf-lib": "^1.3.1",
"query-string": "^6.8.2",
"react": "^16.5.2",
"react-dom": "^16.5.2",
Expand Down
Binary file added public/fonts/arial-unicode-ms.ttf
Binary file not shown.
159 changes: 142 additions & 17 deletions src/server/api/services/getdeckform.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,124 @@
'use strict';
var pdfFillForm = require('pdf-fill-form');
const fs = require('fs');
const fontkit = require('@pdf-lib/fontkit');
const {
PDFDocument,
PDFArray,
PDFHexString,
PDFNumber,
breakTextIntoLines,
PDFOperator,
degrees,
drawLinesOfText,
PDFOperatorNames: Ops,
PDFName,
rgb,
StandardFonts,
asPDFName,
PDFContentStream,
pushGraphicsState,
popGraphicsState,
} = require('pdf-lib');

const getAcroForm = pdfDoc => {
return pdfDoc.catalog.lookup(PDFName.of('AcroForm'));
};

const getAcroFields = pdfDoc => {
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, name) => {
const acroFields = getAcroFields(pdfDoc);
return acroFields.find(acroField => {
const fieldName = acroField.get(PDFName.of('T'));
return !!fieldName && fieldName.value === name;
});
};

const fillAcroTextField = (acroField, text, font, multiline = false) => {
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 N = multiline
? multiLineAppearanceStream(font, text, width, height)
: singleLineAppearanceStream(font, text, 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 =>
PDFOperator.of(Ops.BeginMarkedContent, [asPDFName(tag)]);

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

const singleLineAppearanceStream = (font, text, width, height) => {
const size = 12;
// const lineWidth = font.widthOfTextAtSize(text, fillingSize);
const lines = [font.encodeText(text)];
const x = 0;
const y = height - size;
return textFieldAppearanceStream(font, size, lines, x, y, width, height);
};

const multiLineAppearanceStream = (font, text, width, height) => {
const size = 9;
// const lineWidth = font.widthOfTextAtSize(text, fillingSize);
const textWidth = t => font.widthOfTextAtSize(t, size);
const lines = breakTextIntoLines(text, [' '], width, textWidth).map(line =>
font.encodeText(line),
);
const x = 0;
const y = height - size;
return textFieldAppearanceStream(font, size, lines, x, y, width, height);
};

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

const operators = [
beginMarkedContent('Tx'),
pushGraphicsState(),
...drawLinesOfText(lines, {
color: rgb(0, 0, 0),
font: 'F0',
size: size,
rotate: degrees(0),
xSkew: degrees(0),
ySkew: degrees(0),
x: x,
y: y,
lineHeight: size + 2,
}),
popGraphicsState(),
endMarkedContent(),
];

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

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

import GetDeckById from '../helpers/get-deck-by-id'
import Forms from '../../config/forms';
Expand All @@ -24,6 +143,19 @@ const filterCardQuantity = (cards) =>{

module.exports = async (req, res, next) => {
const Form = req.params.formtype ? Forms[req.params.formtype] : Forms.BSNA;

const notoFontBytes = await fs.readFileSync('../../../public/fonts/arial-unicode-ms.ttf')
const pdfDoc = await PDFDocument.load(fs.readFileSync(Form.path));
pdfDoc.registerFontkit(fontkit);
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman, { subset: true });
const notoFont = await pdfDoc.embedFont(notoFontBytes, { subset: true });

const fillInField = (fieldName, text, font, multiline = false) => {
const field = findAcroFieldByName(pdfDoc, fieldName);
if (!field) throw new Error(`Missing AcroField: ${fieldName}`);
fillAcroTextField(field, text, font, multiline);
};

if(!Form){
res.status(500).json({
success: false,
Expand Down Expand Up @@ -54,9 +186,7 @@ module.exports = async (req, res, next) => {
return 0;
})

let FillData = {
[Form.fields.DeckName]: DeckName
}
fillInField([Form.fields.DeckName], DeckName, timesRomanFont, true)

Form.cardtypes.map( (type) => {
let TypeCards = Cards;
Expand All @@ -66,32 +196,27 @@ module.exports = async (req, res, next) => {
}

TypeCards.map( (card, i) => {

const locale = card.locale[Form.lang].name ? Form.lang : 'NP';
const quantity = type ? Form.fields[type].Quantity(i) : Form.fields.Quantity(i);
const code = type ? Form.fields[type].Code(i) : Form.fields.Code(i);
const level = type ? Form.fields[type].Level(i) : Form.fields.Level(i);
const name = type ? Form.fields[type].Name(i) : Form.fields.Name(i);

//console.log(quantity, code, level, name)

FillData = {
...FillData,
[quantity]: card.quantity,
//TODO change this to static card code when available
[code]: `${card.set}/${card.side}${card.release}${ card.side && card.release ? '-' : '' }${card.sid} ${card.rarity}`,
[level]: card.level,
[name]: card.locale[locale].name,
}
fillInField(quantity, card.quantity, timesRomanFont)
fillInField(code, `${card.set}/${card.side}${card.release}${ card.side && card.release ? '-' : '' }${card.sid} ${card.rarity}`, timesRomanFont)
fillInField(level, card.level, timesRomanFont)
fillInField(name, card.locale[locale].name, locale !== 'JP' ? timesRomanFont : notoFont, true)
})
})

try {
var pdf = pdfFillForm.writeSync(Form.path,
FillData, { "save": "pdf" } );
const pdfBytes = await pdfDoc.save();
const pdfBuffer = Buffer.from(pdfBytes.buffer, 'binary');
res.setHeader('Content-Disposition', 'attachment; filename=' + `${Deck.name}.pdf`);
res.type("application/pdf");
res.send(pdf);
res.send(pdfBuffer);

} catch (error) {
console.log(error);
Expand Down