From f65c8f04be18851263b8c5aea324818ff8636e72 Mon Sep 17 00:00:00 2001 From: Alexander Wunschik Date: Wed, 27 Dec 2023 16:17:12 +0100 Subject: [PATCH] feat(#72): provide command line interface --- README.md | 25 +++++++++ cli.js | 68 ++++++++++++++++++++++ package-lock.json | 17 ++++++ package.json | 4 ++ test/cli.test.js | 109 ++++++++++++++++++++++++++++++++++++ test/fixtures/123.pdf | Bin 0 -> 4728 bytes test/fixtures/123456789.pdf | Bin 0 -> 9123 bytes test/fixtures/2468.pdf | Bin 0 -> 5518 bytes 8 files changed, 223 insertions(+) create mode 100644 cli.js create mode 100644 test/cli.test.js create mode 100644 test/fixtures/123.pdf create mode 100644 test/fixtures/123456789.pdf create mode 100644 test/fixtures/2468.pdf diff --git a/README.md b/README.md index f7a3f49..07bb98a 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,33 @@ This library is inspired by the [PHP library PDFMerger](https://github.com/myoky `npm install --save pdf-merger-js` +of global installation if you want to use the cli tool: + +`npm install -g pdf-merger-js` + ## Usage +### CLI + +``` +Options: + -V, --version output the version number + -o, --output Merged PDF output file path + -v, --verbose Print verbose output + -s, --silent do not print any output to stdout. Overwrites --verbose + -h, --help display help for command +``` + +#### Example calls + +Merge pages 1-2 from the first input with pages 1,2 and 5-7 from the second pdf document: + +`pdf-merge --output ./merged.pdf ./input1.pdf#1-2 ./input2.pdf#1,2,5-7` + +Get two pdf files from the an url and merge the first one with pages 2-3 from the second one: + +`pdf-merge --verbose --output ./sample.pdf Testfile.pdf https://pdfobject.com/pdf/sample.pdf https://upload.wikimedia.org/wikipedia/commons/1/13/Example.pdf#2-3` + ### node.js The node.js version has the following export functions: diff --git a/cli.js b/cli.js new file mode 100644 index 0000000..042fb3c --- /dev/null +++ b/cli.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +import fs from 'fs-extra' +import { program } from 'commander' + +import PDFMerger from './index.js' +import { parsePagesString } from './parsePagesString.js' + +function main (packageJson) { + program + .version(packageJson.version) + .description(packageJson.description) + .option('-o, --output ', 'Merged PDF output file path') + .option('-v, --verbose', 'Print verbose output') + .option('-s, --silent', 'do not print any output to stdout. Overwrites --verbose') + .arguments('') + .action(async (inputFiles, cmd) => { + const outputFile = cmd.output + const verbose = cmd.verbose && !cmd.silent + const silent = cmd.silent + + if (!outputFile) { + console.error('Please provide an output file using the --output flag') + return + } + + if (!inputFiles || !inputFiles.length) { + console.error('Please provide at least one input file') + return + } + + try { + const merger = new PDFMerger() + + for (const inputFile of inputFiles) { + const [filePath, pagesString] = inputFile.split('#') + const pages = pagesString ? parsePagesString(pagesString) : null + if (verbose) { + if (pages && pages.length) { + console.log(`adding page${pages.length > 1 ? 's' : ''} ${pages.join(',')} from ${filePath} to output...`) + } else { + console.log(`adding all pages from ${filePath} to output...`) + } + } + await merger.add(filePath, pages) + } + + if (verbose) { + console.log(`Saving merged output to ${outputFile}...`) + } + + await merger.save(outputFile) + + if (!silent) { + console.log(`Merged pages successfully into ${outputFile}`) + } + } catch (error) { + console.error('An error occurred while merging the PDFs:', error) + } + }) + + program.parse(process.argv) +} + +(() => { + const packageJson = fs.readJsonSync(new URL('./package.json', import.meta.url)) + main(packageJson) +})() diff --git a/package-lock.json b/package-lock.json index ade5dec..1e8f133 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,12 @@ "version": "5.0.0", "license": "MIT", "dependencies": { + "commander": "^11.1.0", "pdf-lib": "^1.17.1" }, + "bin": { + "pdf-merger-js": "cli.js" + }, "devDependencies": { "fs-extra": "^11.2.0", "jest": "^29.7.0", @@ -2054,6 +2058,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -9152,6 +9164,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", diff --git a/package.json b/package.json index eaa6d14..60e452e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "main": "./index.js", "types": "./index.d.ts", "browser": "./browser.js", + "bin": { + "pdf-merge": "cli.js" + }, "scripts": { "standard": "standard", "standard:fix": "standard --fix", @@ -29,6 +32,7 @@ "author": "nbesli", "license": "MIT", "dependencies": { + "commander": "^11.1.0", "pdf-lib": "^1.17.1" }, "devDependencies": { diff --git a/test/cli.test.js b/test/cli.test.js new file mode 100644 index 0000000..ed4d4f4 --- /dev/null +++ b/test/cli.test.js @@ -0,0 +1,109 @@ +import path from 'path' +import fs from 'fs-extra' +import util from 'util' +import { exec } from 'child_process' +import { jest } from '@jest/globals' +import pdfDiff from 'pdf-diff' + +const asyncExec = util.promisify(exec) + +const __dirname = path.dirname(new URL(import.meta.url).pathname) +const FIXTURES_DIR = path.join(__dirname, 'fixtures') +const TMP_DIR = path.join(__dirname, 'tmp') + +jest.setTimeout(10000) + +async function mergePDFsCli (outputFile, inputFiles) { + const { stdout, stderr } = await asyncExec(`node ./cli.js --output ${outputFile} ${inputFiles.join(' ')}`) + return { stdout, stderr } +} + +describe('issues', () => { + beforeAll(async () => { + await fs.ensureDir(TMP_DIR) + }) + + test('should merge two pdfs', async () => { + await mergePDFsCli(path.join(TMP_DIR, 'Testfile_AB.pdf'), [ + path.join(FIXTURES_DIR, 'Testfile_A.pdf'), + path.join(FIXTURES_DIR, 'Testfile_B.pdf') + ]) + const diff = await pdfDiff( + path.join(FIXTURES_DIR, 'Testfile_AB.pdf'), + path.join(TMP_DIR, 'Testfile_AB.pdf') + ) + expect(diff).toBeFalsy() + }) + + test('should merge two pdfs from URL', async () => { + await mergePDFsCli(path.join(TMP_DIR, 'Testfile_AB.pdf'), [ + 'https://github.com/nbesli/pdf-merger-js/raw/master/test/fixtures/Testfile_A.pdf', + 'https://github.com/nbesli/pdf-merger-js/raw/master/test/fixtures/Testfile_B.pdf' + ]) + const diff = await pdfDiff( + path.join(FIXTURES_DIR, 'Testfile_AB.pdf'), + path.join(TMP_DIR, 'Testfile_AB.pdf') + ) + expect(diff).toBeFalsy() + }) + + test('combine single pages from multiple pdfs', async () => { + await mergePDFsCli(path.join(TMP_DIR, '2468.pdf'), [ + path.join(FIXTURES_DIR, '123456789.pdf#2'), + path.join(FIXTURES_DIR, '123456789.pdf#4'), + path.join(FIXTURES_DIR, '123456789.pdf#6'), + path.join(FIXTURES_DIR, '123456789.pdf#8') + ]) + const diff = await pdfDiff( + path.join(FIXTURES_DIR, '2468.pdf'), + path.join(TMP_DIR, '2468.pdf') + ) + expect(diff).toBeFalsy() + }) + + test('combine pages from multiple pdfs (list)', async () => { + await mergePDFsCli(path.join(TMP_DIR, '2468.pdf'), [ + path.join(FIXTURES_DIR, '123456789.pdf#2,4'), + path.join(FIXTURES_DIR, '123456789.pdf#6,8') + ]) + const diff = await pdfDiff( + path.join(FIXTURES_DIR, '2468.pdf'), + path.join(TMP_DIR, '2468.pdf') + ) + expect(diff).toBeFalsy() + }) + + test('combine pages from multipel pdfs (rang)', async () => { + await mergePDFsCli(path.join(TMP_DIR, '123456789.pdf'), [ + path.join(FIXTURES_DIR, '123456789.pdf#1-5'), + path.join(FIXTURES_DIR, '123456789.pdf#6-9') + ]) + const diff = await pdfDiff( + path.join(FIXTURES_DIR, '123456789.pdf'), + path.join(TMP_DIR, '123456789.pdf') + ) + expect(diff).toBeFalsy() + }) + + test('combine pages from multipel pdfs (combined list and range)', async () => { + await mergePDFsCli(path.join(TMP_DIR, '123456789.pdf'), [ + path.join(FIXTURES_DIR, '123456789.pdf#1,2,3-4'), + path.join(FIXTURES_DIR, '123456789.pdf#5-7,8to9') + ]) + const diff = await pdfDiff( + path.join(FIXTURES_DIR, '123456789.pdf'), + path.join(TMP_DIR, '123456789.pdf') + ) + expect(diff).toBeFalsy() + }) + + afterEach(async () => { + for (const file of await fs.readdir(TMP_DIR)) { + await fs.unlink(path.join(TMP_DIR, file)) + } + }) + + afterAll(async () => { + await fs.remove(TMP_DIR) + }) +}) diff --git a/test/fixtures/123.pdf b/test/fixtures/123.pdf new file mode 100644 index 0000000000000000000000000000000000000000..90032edd463f3ad6fa60c444f09efc80728c9247 GIT binary patch literal 4728 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 LlCiP9y94Pz2zEz{ literal 0 HcmV?d00001 diff --git a/test/fixtures/123456789.pdf b/test/fixtures/123456789.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d226a4c6dc4f4ec8267a698210951a4310866d86 GIT binary patch literal 9123 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 literal 0 HcmV?d00001