diff --git a/index.js b/index.js index d2f9561..7397cee 100644 --- a/index.js +++ b/index.js @@ -246,6 +246,103 @@ class PdfTk { } + /** + * Takes a pdftk info string and turns it into an object. + * @static + * @public + * @param {string} data + * @returns {Object} Key value pairs and arrays of info data. + */ + static infoStringToObject(data) { + if (!data || !PdfTk.isString(data)) return null; + const KEY_DIVIDER = 'Begin'; + const singleLines = data.split('\n'); + let curKey = null; + const serialised = singleLines.reduce((acc, row) => { + const splitRow = row.split(KEY_DIVIDER); + //key could be Info, or Bookmark, or PageMedia, etc. + const key = splitRow[0]; + //Key with -Begin- has encountered + if (row.indexOf(KEY_DIVIDER) > -1) { + //create a new index of data key if it does not exist yet + if (!acc.hasOwnProperty(key)) { + curKey = key; + acc[key] = []; + } + //a new row with 'Begin' always warants a new object to hold its values + acc[key].push({}); + //and then return, as we don't add new entries at this point + return acc; + } + const container = acc[curKey]; + const currentEntry = container[container.length - 1]; + //contains the row minus the main key + const valueContainer = row.substring(curKey.length); + + //check if current value is part of a parent key or a simple key value pair + if (valueContainer && row.substring(0, curKey.length) === curKey) { + const valueKey = valueContainer.split(':'); + const key = valueKey.shift(); // valueKey[0] is always key + let value = valueKey.shift(); // valueKey[1] is always value + if (valueKey.length) { + //if value contains ':' it got split as well, join remainder + value = `${value}:${valueKey.join(':')}`; + } + currentEntry[key] = value.trim(); + } else { + //item is not on key, and should just be added as is + const splitColon = row.split(':'); + if (splitColon[1]) { + acc[splitColon[0]] = splitColon[1].trim(); + } + } + + return acc; + }, {}); + + return serialised; + } + /** + * Creates a pdftk info string value from an object. + * + * @static + * @public + * @param {Object} data + * @returns {String} Concatenated string value which can be passed to pdftk + */ + static infoObjectToString(data) { + if (!data || !PdfTk.isObject(data)) return null; + return Object.keys(data) + .reduce((acc, key) => { + const val = data[key]; + //if value is array, split it and create a + //new string row with [key]Begin (BookmarkBegin,InfoBegin, etc.) + if (Array.isArray(val)) { + const vals = val.reduce((acc, item) => { + const innerValues = Object.keys(item).reduce( + (innerAcc, innerKey, innerIndex) => { + if (innerIndex === 0) { + innerAcc = `${innerAcc}\n${key}Begin`; + } + innerAcc = `${innerAcc}\n${key}${innerKey}: ${ + item[innerKey] + }`; + return innerAcc; + }, + '' + ); + + return `${acc}${innerValues}`; + }, ''); + return `${acc}${vals}`; + } + // if not an array, take as is and add to accumulator with newline + // for instance; `NumberOfPages: 6` + return `${acc}\n${key}: ${val}`; + }, '') + .trim(); + } + /** * Creates pdf info text file from JSON input. * @static @@ -254,19 +351,7 @@ class PdfTk { * @returns {Buffer} Info text file as a buffer. */ static generateInfoFromJSON(data) { - const info = []; - for (const prop in data) { - /* istanbul ignore else */ - if (data.hasOwnProperty(prop)) { - const begin = PdfTk.stringToBuffer('InfoBegin\nInfoKey: '); - const key = PdfTk.stringToBuffer(prop.toString()); - const newline = PdfTk.stringToBuffer('\nInfoValue: '); - const value = PdfTk.stringToBuffer(data[prop].toString()); - const newline2 = PdfTk.stringToBuffer('\n'); - info.push(begin, key, newline, value, newline2); - } - } - return Buffer.concat(info); + return PdfTk.stringToBuffer(PdfTk.infoObjectToString(data)); } /** @@ -1072,6 +1157,7 @@ class PdfTk { // module.exports = PdfTk; module.exports = { + PdfTk, input: file => new PdfTk(file), configure: options => { Object.defineProperties(PdfTk.prototype, { diff --git a/test/updateInfo.spec.js b/test/updateInfo.spec.js new file mode 100644 index 0000000..d183821 --- /dev/null +++ b/test/updateInfo.spec.js @@ -0,0 +1,125 @@ +/* globals describe, it */ +const chai = require('chai'); + +const { expect, } = chai; + +const pdftk = require('../'); +const path = require('path'); + +const sample = `InfoBegin +InfoKey: ModDate +InfoValue: D:20190809100746+10'00' +InfoBegin +InfoKey: CreationDate +InfoValue: D:20190809100746+10'00' +InfoBegin +InfoKey: Creator +InfoValue: pdftk 2.02 - www.pdftk.com +InfoBegin +InfoKey: Producer +InfoValue: itext-paulo-155 (itextpdf.sf.net-lowagie.com) +PdfID0: b65ba3f658853c3c8df5d33b06e31195 +PdfID1: b9f57d4e3b9b1162bb14654fe4d534c0 +NumberOfPages: 6 +BookmarkBegin +BookmarkTitle: Bookmark test Level 1 +BookmarkLevel: 1 +BookmarkPageNumber: 1 +BookmarkBegin +BookmarkTitle: Bookmark test Level 2 +BookmarkLevel: 2 +BookmarkPageNumber: 1 +BookmarkBegin +BookmarkTitle: Bookmark test Level 2-2 +BookmarkLevel: 2 +BookmarkPageNumber: 2 +BookmarkBegin +BookmarkTitle: Bookmark test Level 3 +BookmarkLevel: 3 +BookmarkPageNumber: 5 +PageMediaBegin +PageMediaNumber: 1 +PageMediaRotation: 0 +PageMediaRect: 0 0 594.96 841.92 +PageMediaDimensions: 594.96 841.92 +PageMediaBegin +PageMediaNumber: 2 +PageMediaRotation: 0 +PageMediaRect: 0 0 594.96 841.92 +PageMediaDimensions: 594.96 841.92 +PageMediaBegin +PageMediaNumber: 3 +PageMediaRotation: 0 +PageMediaRect: 0 0 594.96 841.92 +PageMediaDimensions: 594.96 841.92 +PageMediaBegin +PageMediaNumber: 4 +PageMediaRotation: 0 +PageMediaRect: 0 0 594.96 841.92 +PageMediaDimensions: 594.96 841.92 +PageMediaBegin +PageMediaNumber: 5 +PageMediaRotation: 0 +PageMediaRect: 0 0 594.96 841.92 +PageMediaDimensions: 594.96 841.92 +PageMediaBegin +PageMediaNumber: 6 +PageMediaRotation: 0 +PageMediaRect: 0 0 594.96 841.92 +PageMediaDimensions: 594.96 841.92`; + +describe('updateInfo', function () { + + it('serialise and deserialise the info object with equal results', function () { + const serialised = pdftk.PdfTk.infoStringToObject(sample); + const deserialised = pdftk.PdfTk.infoObjectToString(serialised); + expect(sample).to.equal(deserialised); + }); + it('should parse info from dumpdata correctly to show number of pages', function () { + const input = path.join(__dirname, './files/document1.pdf'); + return pdftk + .input(input) + .dumpData() + .output() + .then(buffer => pdftk.PdfTk.infoStringToObject(buffer.toString('utf8'))) + .then(object => expect(parseInt(object.NumberOfPages)).to.equal(5)); + + }); + it('should write output file with added bookmarks', function () { + const strToObj = pdftk.PdfTk.infoStringToObject; + const input = path.join(__dirname, './files/document1.pdf'); + const output = path.join(__dirname, './files/updateinfo.temp.pdf'); + const newBookmark = { Title: 'Bookmark test title', Level: '1', PageNumber: '1', }; + + const dumpData = () => pdftk + .input(input) + .dumpData() + .output() + .then(buffer => buffer.toString('utf8')); + + const addBookmark = () => + dumpData().then(data => strToObj(data)).then(infoObject => { + infoObject.Bookmark = [ + newBookmark, + ]; + return infoObject; + }); + + const writeFileWithUpdates = () => + addBookmark().then(infoObject => + pdftk + .input(input) + .updateInfo(infoObject) + .output(output)); + + + return writeFileWithUpdates().then(() => + pdftk + .input(output) + .dumpData() + .output() + .then(buffer => pdftk.PdfTk.infoStringToObject(buffer.toString('utf8'))) + .then(obj => expect(obj.Bookmark[0].Title).to.equal(newBookmark.Title))); + + }); +});