diff --git a/package.json b/package.json index 585fdf51d0e..c85483af2bc 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "markdown-toc": "^1.1.0", "minimist": "^1.2.0", "ncp": "^2.0.0", + "pdfjs-dist": "^1.8.595", "pixelmatch": "^4.0.2", "pngjs": "^3.2.0", "text-diff": "^1.0.1" diff --git a/test/test.js b/test/test.js index a45cd0f6994..86ea26f4be9 100644 --- a/test/test.js +++ b/test/test.js @@ -1364,17 +1364,79 @@ describe('Page', function() { })); }); - describe('Page.pdf', function() { - const outputFile = __dirname + '/assets/output.pdf'; - afterEach(function() { - fs.unlinkSync(outputFile); - }); - - // Printing to pdf is currently only supported in headless - (headless ? it : xit)('should print to pdf', SX(async function() { - await page.navigate(PREFIX + '/grid.html'); + // Printing to pdf is currently only supported in headless + (headless ? describe : xdescribe)('Page.pdf', function() { + it('should be able to save file', SX(async function() { + const outputFile = __dirname + '/assets/output.pdf'; await page.pdf({path: outputFile}); expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0); + fs.unlinkSync(outputFile); + })); + it('should default to printing in Letter format', SX(async function() { + let pages = await getPDFPages(await page.pdf()); + expect(pages.length).toBe(1); + expect(pages[0].width).toBeCloseTo(8.5, 1e-2); + expect(pages[0].height).toBeCloseTo(11, 1e-2); + })); + it('should support setting custom format', SX(async function() { + let pages = await getPDFPages(await page.pdf({ + format: 'A4' + })); + expect(pages.length).toBe(1); + expect(pages[0].width).toBeCloseTo(8.27, 1e-2); + expect(pages[0].height).toBeCloseTo(11.7, 1e-2); + })); + it('should support setting paper width and height', SX(async function() { + let pages = await getPDFPages(await page.pdf({ + width: '10in', + height: '10in', + })); + expect(pages.length).toBe(1); + expect(pages[0].width).toBeCloseTo(10, 1e-2); + expect(pages[0].height).toBeCloseTo(10, 1e-2); + })); + it('should print multiple pages', SX(async function() { + await page.navigate(PREFIX + '/grid.html'); + // Define width and height in CSS pixels. + const width = 50 * 5 + 1; + const height = 50 * 5 + 1; + let pages = await getPDFPages(await page.pdf({width, height})); + expect(pages.length).toBe(8); + expect(pages[0].width).toBeCloseTo(cssPixelsToInches(width), 1e-2); + expect(pages[0].height).toBeCloseTo(cssPixelsToInches(height), 1e-2); + })); + it('should support page ranges', SX(async function() { + await page.navigate(PREFIX + '/grid.html'); + // Define width and height in CSS pixels. + const width = 50 * 5 + 1; + const height = 50 * 5 + 1; + let pages = await getPDFPages(await page.pdf({width, height, pageRanges: '1,4-7'})); + expect(pages.length).toBe(5); + })); + it('should throw if format is unknown', SX(async function() { + let error = null; + try { + await getPDFPages(await page.pdf({ + format: 'something' + })); + } catch (e) { + error = e; + } + expect(error).toBeTruthy(); + expect(error.message).toContain('Unknown paper format'); + })); + it('should throw if units are unknown', SX(async function() { + let error = null; + try { + await getPDFPages(await page.pdf({ + width: '10em', + height: '10em', + })); + } catch (e) { + error = e; + } + expect(error).toBeTruthy(); + expect(error.message).toContain('Failed to parse parameter value'); })); }); @@ -1529,6 +1591,39 @@ function waitForEvents(emitter, eventName, eventCount = 1) { } } +/** + * @param {!Buffer} pdfBuffer + * @return {!Promise>} + */ +async function getPDFPages(pdfBuffer) { + const PDFJS = require('pdfjs-dist'); + PDFJS.disableWorker = true; + const data = new Uint8Array(pdfBuffer); + const doc = await PDFJS.getDocument(data); + let pages = []; + for (let i = 0; i < doc.numPages; ++i) { + let page = await doc.getPage(i + 1); + let viewport = page.getViewport(1); + // Viewport width and height is in PDF points, which is + // 1/72 of an inch. + pages.push({ + width: viewport.width / 72, + height: viewport.height / 72, + }); + page.cleanup(); + } + doc.cleanup(); + return pages; +} + +/** + * @param {number} px + * @return {number} + */ +function cssPixelsToInches(px) { + return px / 96; +} + // Since Jasmine doesn't like async functions, they should be wrapped // in a SX function. function SX(fun) { diff --git a/yarn.lock b/yarn.lock index b9483ffc7a9..5f79848b319 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,6 +27,15 @@ ajv@^4.7.0: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^5.0.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + ansi-escapes@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b" @@ -92,6 +101,10 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +big.js@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978" + bindings@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11" @@ -216,6 +229,10 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + "entities@~ 1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" @@ -337,6 +354,10 @@ extract-zip@^1.6.5: mkdirp "0.5.0" yauzl "2.4.1" +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -585,12 +606,20 @@ jschardet@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.4.2.tgz#2aa107f142af4121d145659d44f50830961e699a" +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + json-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" dependencies: jsonify "~0.0.0" +json5@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -633,6 +662,14 @@ list-item@^1.1.1: is-number "^2.1.0" repeat-string "^1.5.2" +loader-utils@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + lodash@^4.0.0, lodash@^4.17.4, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -727,6 +764,10 @@ ncp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" +node-ensure@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" + object-assign@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -772,6 +813,13 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" +pdfjs-dist@^1.8.595: + version "1.8.599" + resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-1.8.599.tgz#20d55654f3b3b1a8394649a074d0baa1bd54060f" + dependencies: + node-ensure "^0.0.0" + worker-loader "^0.8.0" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -898,6 +946,12 @@ safe-buffer@~5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223" +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + set-getter@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" @@ -1024,6 +1078,13 @@ wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" +worker-loader@^0.8.0: + version "0.8.1" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-0.8.1.tgz#e8e995331ea34df5bf68296824bfb7f0ad578d43" + dependencies: + loader-utils "^1.0.2" + schema-utils "^0.3.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"