diff --git a/README.md b/README.md index 18cc45e..7765536 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,12 @@ - **PDF** → Images (JPG) - **PDF** → Text (TXT) - Extract text content from PDF files - **PDF Info** → Display metadata (title, creator, page count, file size) +- Merge multiple PDFs #### Coming Soon - PowerPoint → PDF - Excel → PDF - HTML → PDF -- Merge multiple PDFs - PDF compression - PDF text extraction @@ -158,6 +158,22 @@ filto extract input.pdf -o extracted.txt ``` filto extract input.pdf --page 1 --output page1.txt ``` +--- +### 5. Merge PDF Files +Merge multiple PDF files into a single PDF + +**Basic usage:** +``` +filto merge file1.pdf file2.pdf output.pdf +``` + +**Example with multiple files:** +``` +filto merge doc1.pdf doc2.pdf doc3.pdf merged_output.pdf +``` + +> **Note:** You need to provide at least two PDF files to merge. The last argument will be used as the output file. + --- ## Contributing diff --git a/package-lock.json b/package-lock.json index 5ac511a..858253f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@oclif/plugin-plugins": "^5", "asposepdfnodejs": "^25.10.0", "image-to-pdf": "^3.0.2", + "pdf-lib": "^1.17.1", "pdf-parse": "^2.4.5", "rewiremock": "^3.14.6" }, @@ -2900,6 +2901,36 @@ "@oclif/core": ">= 3.0.0" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/standard-fonts/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, + "node_modules/@pdf-lib/upng/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -12335,6 +12366,30 @@ "node": ">= 0.10" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pdf-parse": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", diff --git a/package.json b/package.json index d633b20..be8b98d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@oclif/plugin-plugins": "^5", "asposepdfnodejs": "^25.10.0", "image-to-pdf": "^3.0.2", + "pdf-lib": "^1.17.1", "pdf-parse": "^2.4.5", "rewiremock": "^3.14.6" }, diff --git a/src/commands/merge/index.ts b/src/commands/merge/index.ts new file mode 100644 index 0000000..d707b31 --- /dev/null +++ b/src/commands/merge/index.ts @@ -0,0 +1,35 @@ +import { Args, Command, Flags } from '@oclif/core' +import { MergePdfs } from '../../merge/index.js'; +export default class Merge extends Command { + static override description = ' Merge multiple PDFs' + + static override args = { + inputs: Args.string({ description: 'Input PDF files to merge', required: true, multiple: true }), + output: Args.string({ description: 'Output merged PDF file', required: true }), + } + static override strict = false; + + public async run(): Promise { + const { args } = await this.parse(Merge) + const { inputs } = args; + + if (inputs.length < 2) { + this.error('Please provide at least two PDF files to merge.'); + } + + try { + const argv = process.argv; + const inputsArr = argv.slice(3, argv.length - 1); + const result = await MergePdfs(inputsArr, argv[argv.length - 1]); + + if (result.success) { + this.log(result.message); + } else { + this.error(result.message); + } + } catch (error: any) { + this.error(`Unexpected error during merge: ${error.message}`); + } + + } +} diff --git a/src/merge/index.ts b/src/merge/index.ts new file mode 100644 index 0000000..ccc231c --- /dev/null +++ b/src/merge/index.ts @@ -0,0 +1,42 @@ + +import { PDFDocument } from 'pdf-lib'; +import fs from 'fs'; +import ConversionResult from '../types/converstionResult.js'; + +export async function MergePdfs(inputPdf: string[], output: string = "merged.pdf"): Promise { + if (!fs.existsSync(inputPdf[0])) { + return { + success: false, + message: `Input file not found: ${inputPdf}` + }; + } + + try { + const pdfToMerge = inputPdf.map(filePath => fs.readFileSync(filePath)); + + const mergedPdf = await PDFDocument.create(); + for (const pdfBytes of pdfToMerge) { + const pdf = await PDFDocument.load(pdfBytes); + const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); + copiedPages.forEach((page) => { + mergedPdf.addPage(page); + }); + } + + const buf = await mergedPdf.save(); + fs.writeFileSync(output, buf); + + return { + success: true, + message: `Successfully Merged PDFs: ${inputPdf.map(pdf => { + return pdf + })} into -> ${output}` + }; + } catch (error: any) { + return { + success: false, + message: `Error during Merge: ${error.message}`, + error + }; + } +} \ No newline at end of file