diff --git a/package.json b/package.json index d90ddfc..c15fbb6 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/public/fonts/arial-unicode-ms.ttf b/public/fonts/arial-unicode-ms.ttf new file mode 100755 index 0000000..51a18bc Binary files /dev/null and b/public/fonts/arial-unicode-ms.ttf differ diff --git a/src/server/api/services/getdeckform.js b/src/server/api/services/getdeckform.js index 98266f1..dd605b7 100644 --- a/src/server/api/services/getdeckform.js +++ b/src/server/api/services/getdeckform.js @@ -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'; @@ -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, @@ -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; @@ -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);