Pdf állományok egyesítése

Pdf állományok egyesítése

CLI Fájl műveletek Node.js npm

2023. január 08.

Nagyon sokszor fordul elő olyan eset, hogy több pdf állományból kell egyet készíteni. Ezt mindenki különböző megoldásokkal igyekszik megvalósítani. Ehhez léteznek jó online eszközök is, és ezen túl is van több lokális lehetőség, például Linuxon nagyon jó parancssori eszközök érhetőek el. Viszont előfordulhat olyan eset, amikor nem jöhet szóba egy felhő alapú szolgáltatás használata sem. Legyen itt szó embargós adatokról, vagy üzleti titokról vagy bármi más okról, ami kizárja az online alkalmazásokat. A lényeg, hogy az állományok nem adhatóak ki harmadik félnek és így nem lehet őket sehova feltölteni.

Ebben az esetben vagy van már valami lokális programunk, amit használunk és bevált, vagy kézzel, pdf szerkesztőben másoljuk össze az állományokat, azonban ez elég sok időt vehet igényben. Amikor egyszer vagy kétszer fordul elő ilyen, akkor az senkit sem zavar, de ha többször is előkerül ez a munkafolyamat, akkor az nagyon sok munkaórát tud elvinni az értékes időből. Ebben a bejegyzésben egy egyszerű pdf összefűző program elkészítését fogom megmutatni NodeJs használatával.

Első lépésben nézzük meg, hogy létezik-e már olyan csomag, ami ezt a funkciót valamilyen módon tudja. Az alapelv az, hogy ha ezt már valaki megírta jól, akkor feleslegesen ne írjuk meg újra. Jelen esetben szerencsénk van, mert több csomag is létezik és lehet válogatni közülük. Én kiválasztottam pdf-merger-js nevű csomagot, többek között azért, mert sokan használják, több fejlesztője is van illetve gyakran kapott frissítéseket és javításokat az utóbbi időben. Ezen túl még az alap program jól paraméterezhető és ezáltal plusz funkciók érhetőek el a számunkra.

A pdf-merger-js itt található meg: https://www.npmjs.com/package/pdf-merger-js

Amennyiben közvetlenül ezt a programkönyvtárat szeretnénk használni, akkor ahhoz minden alkalommal a forráskódba kellene átírni az összefűzendő pdf állományok nevét és az esetleges oldalszámokat. Természetesen ez a legegyszerűbb megoldás, de ha már foglalkozunk vele, akkor érdemes lenne egy terminálban használható, paraméterezhető alkalmazást írni. Ennek az a legnagyobb előnye, hogy amikor használjuk, akkor már nem kell kódolni. Ehelyett csak annyi a teendőnk, hogy az általunk készített programot futtatjuk és paraméterként átadjuk az értékeket, amivel dolgoznia kell.

Jelen esetben meg kell adni azokat az állományokat, amiket szeretnénk egyesíteni. Ezen túl jó lenne paraméterezni, hogy az egyes dokumentumból milyen oldalakat szeretnénk látni az új állományunkban. Az sem lenne hátrány, ha meg tudnánk adni az új egyesített pdf állomány fájl nevét, ezzel is könnyítve a késöbbi megtalálását.

Lássunk is neki…

Első lépésünk a pdf-merger telepítése és használata. A node csomagok telepítéséről és használatáról már korábban volt egy bejegyzés.

$ npm install pdf-merger

Ezt követően a kódunkban implementáljuk ezt a függvénykönyvtárat és ezzel már használatra kész.

Megjegyzés: Az egyszerűség kedvéért én az alap állománynak az index.js-t hoztam létre és ebben dolgozom.

const PDFMerger = require("pdf-merger-js");

Az ezt következő lépés, hogy beolvassuk és feldolgozzuk a terminálból beérkezett paramétereket. Ezeket a nodeJS-ben a process változó argv nevű argumentumában találjuk. Itt egy tömböt kapunk, amibe benne van minden – a program indítása során keletkező és azon túl az általunk megadott – érték.

Ezt tegyük is egy változóba, hogy könnyebben és rövidebben lehessen rá hivatkozni.

const argv = process.argv;

Amennyiben megvizsgáljuk a változónk értékét (például a console-ra való kiírással), akkor láthatjuk, hogy az első értéke maga a nodeJS elérési útvonala, a második pedig az adott, futtatni kívánt állomány teljes elérhetősége. Ez a kettő minden esetben fix és csak ezután következnek a plusz paraméterek, amiket a futtatás során magunk adtunk meg.

Egy ilyen CLI program készítése során a legfontosabb megtervezni a paraméterek listáját, amiket várunk, majd ezeknél meg kell határozni, hogy melyek a kötelezően megadandók és melyek azok a paraméterek, amelyek csak opcionálisok.

A mi esetünkben, mindenféleképpen kötelező megadni legalább egy pdf állományt. Azért csak egyet, mert ezzel lehetőséget biztosítunk, hogy egy több oldalas állományból csak néhány – a számunkra szükséges – oldalt tudjuk kivenni. Ez a paraméter az, ami kötelező minden esetben és ehhez párosul még a többi opcionális paraméter, mint az oldalszám és a mentésre kerülő fájl neve. Az állomány név abban tér el az oldalszámtól, hogy a programban előre definiálni fogunk egy alapértelmezett fájl nevet arra az esetre, ha nem kapunk ilyen paramétert. Így a végeredmény minden esetben mentésre kerül és a szoftvernek előre meg van adva, hogy milyen néven mentse le a végső dokumentumot.

let output = "merged.pdf";

Megjegyzés: Azért let-tel és nem const-al deklaráltam, mert a let módosítható később is, a const értéke csak a létrehozáskor adható meg. Nekünk pedig szükségünk van a módosításra akkor amikor kapunk egy másik fájlnevet a mentéshez.

Egy CLI használata során ezeket a plusz paramétereket úgynevezett kapcsolókkal (flag) szokás megadni. A kapcsolók feldolgozására több megoldás is van, ami megkönnyítheti a munkánk. Viszont ennél a konkrét feladatnál nem tudjuk ezeket az előre elkészített eszközöket használni, mert minden állományhoz engedni kell egy oldalszám kapcsolót. Tehát nem szabad egyben kezelni a kapcsolókat, mindenféleképpen párosítani kell az adott dokumentumhoz.

A kapcsolókat az alábbi módon szokás jelezni: –kapcsoló. Illetve van egy rövidített változat is: -k

Ebben a programban két kapcsolót fogunk használni az oldalak (page) és a kimenet fájlnév (out). Tehát a kapcsolóink a következők:

  • --page vagy röviden -p
  • --out vagy röviden -o

Első lépésként menjünk végig a paraméter tömbön és dolgozzuk fel.

for (i = 0; i < argv.length; i++) {
	//Itt jön majd az ellenőrzés blokk
}

Nézzük meg az adott paramétert, hogy pdf állomány e? Ezt egyszerűen az endsWith() függvénnyel tehetjük meg.

argv[i].endsWith(".pdf");

Erre írjuk is egy rövid függvényt, mert egy párszor használni fogjuk a későbbiek során.

endsWith();

Ha ezzel megvagyunk, akkor kezdjük is a kapcsolók feldolgozását. Nézzük először a legegyszerűbbet a mentési fájlnév paramétert. Ellenőrizzük, hogy a kapcsoló --out vagy -o, ha az akkor a következő paraméter a fájlnév, ez igazából ennyire egyszerű.

Ha már a pdf ellenőrző is külön függvény akkor erre érdemes írni egy külön függvényt, az átláthatóbb kód érdekében.

--out

Tehát ha a kapcsoló az output, akkor már csak egy dolgunk maradt. Meg kell nézni, hogy az adott kapcsoló utána következő paraméter létezik-e és valid pdf állomány név. Mert csak akkor szabad elfogadni.

Ha ezek a feltételek megfelelőek, akkor módosítjuk a output fájl nevet, hisz kézzel meglett adva az a fájlnév amire menteni fogunk. Ezt követően növeljük a ciklus kulcsát, hogy a fájlnév paramétert át ugorja, hiszen annak a feldolgozása már megtörtént és nincs szükség a további vizsgálatára. Majd folytatjuk a ciklust a continue utasítással, hisz ezen a paraméteren már nincs mit feldolgozni.

if (isOutput(argv[i]) && argv[i + 1] && isPDF(argv[i + 1])) {
output = argv[i + 1];
++i;
continue;
}

A következő lépés, hogy az összes olyan pdf-et is fel kell dolgozni, ami nem output. Itt fontos a sorrend, hisz ha előbb dolgozzunk fel az állományokat, mint ahogy az outputot ellenőrizzük, akkor az kimeneti állománynév is csak egy pdf elemként szerepelne a listában, ami a feldolgozás során hibát adna, hisz nem létező állományt próbálja egyesíteni.

Az állományoknak létrehozunk egy tömb változót még a ciklus előtt. Ebbe tesszük majd bele az összes feldolgozandó pdf elérési útvonalát.

const allPdfList = [];

Majd a ciklusba visszatérve ellenőrizzük, hogy egyáltalán pdf-re végződik e paraméter és ha igen akkor csak simán hozzáadunk a tömbhöz egy objektumot. Ebben az objektumban csak a pdf állomány kerül bele egyelőre a pdf kulccsal. Majd léptetjük a ciklust, hisz ezzel a paraméterrel nincs már dolgunk.

if (isPDF(argv[i])) {
allPdfList.push({ pdf: argv[i] });
continue;
}

Tovább haladva, ha a korábbi feltételek nem teljesültek, már csak egy dolgot kell ellenőrizni, az oldal kapcsolót. Tehát ha nem kimeneti paraméter és nem is pdf állomány, akkor meg kell nézni, hogy oldal kapcsoló van e az adott paraméter értékében. Meg kell nézni, hogy --out vagy -o a tartalma.

Ehhez a kapcsoló ellenőrzéshez is létrehoztam egy függvényt, hiszen ha a többi kapcsolónak és a pdf-nek is van akkor ez se különbözzön.

const isPageFlag = (arg) => arg === "-p" || arg === "--page";

Majd meg kell győződni róla, hogy nem ez az utolsó paraméter a tömbben, hisz a kapcsoló után várunk egy értéket. Plusz ellenőrizni kell azt is, hogy mi pontosan a következő érték. Az biztosan tudjuk, hogy nem pdf állományt várunk. Tehát ha tényleg nem az, akkor a következőnek egy oldalra vonatkozó értéknek kell lenni.

Ebben az esetben a feltétel és a feldolgozás így néz ki:

if (isPageFlag(argv[i]) && argv[i + 1] && !isPDF(argv[i + 1])) {
	let pages = argv[i + 1];
	if (!pages.includes("-") && !pages.includes(",") && !pages.includes("to")) {
		pages = [pages];
	}
	allPdfList[allPdfList.length - 1].page = pages;
}

Amennyiben a fenti feltételek igazak akkor is még szükségünk van egy plusz ellenőrzésre és feldolgozásra. Ez azért van mert az eredetileg felhasznált szoftver könyvtár több fajta bemenetet biztosít és ezt érdemes megtartani, mert így több lehetőségünk marad.

Lehet ez a bemenet csak egy szám (pl 3), ebben az esetben csak adott oldalt rakja bele a dokumentumból. Beszélhetünk több oldalról is ekkor vesszővel kell elválasztani őket (pl 1,3,6), ilyenkor minden felsorolt oldalt bele teszi a kimenetbe.

Illetve lehet bizonyos oldalszámtól egy másikig tartó tartományt is megadni. Erre két lehetőséget biztosít a szoftver, az egyik a kötőjellel való elválasztást (pl 1-5) vagy a másik az angol to használatát (1 to 5).

Ennek ismeretében a kódra visszatérve láthatjuk, hogy ha nem tartalmaz kötőjelet, vesszőt és to szócskát az adott paraméter, akkor sima oldalszám. Amire akkor is oda kell figyelni, hogy sima szám, mert a szoftver tömböt vár így azt is kell átadni neki. Minden más esetben csak simán magát az értéket várja.

Amikor megvan a megfelelően átalakított értékünk az átadáshoz, akkor a legutolsó objektumhoz hozzáadjuk. Méghozzá a page mint oldal kulcshoz rendeljük az értéket.

Itt fontos megemlíteni, hogy ugyanazokat a paramétereket várjuk, amit az eredeti szoftver is biztosít. Így csak minimálisan van szükség az értékek átalakítására és feldolgozására, mielőtt tovább adjuk a használt függvénykönyvtárnak.

Ezzel elkészült az pdf listánk a hozzá tartozó oldalakkal párosítva, amennyiben nem teljes dokumentumot használunk, hisz ha teljes dokumentum, akkor nem lesz page értéke az objektumban. Ezeket meg is találjuk az allPdfList nevű változóban.

Még az adatok továbbadása előtt vár ránk egy utolsó ellenőrzés. Meg kell nézni, hogy tényleg van tartalma a pdf-eket tartalmazó változónak. Ugyanis ha üres akkor nem lett megadva pdf és nincs mit egyesíteni. Az azért fontos mert, ha üresen adjunk át, akkor csak hibára fut a program, ezért ezt már előre kezeljük le.

if (allPdfList.length <= 0) {
	console.log("PDF file is required!");
	process.exit(0);
}

Tehát ha mégis üres lenne, akkor dobjon egy hiba jelzést a console-ra és fejeződjön be a program. Ezzel kezelve ezt a hibalehetőséget.

Az utolsó lépés, az egyesítésért felelős PDFMerger-t létrehozni és átadni neki az előkészített tömböt. Majd megvárni amíg a mentés elkészül.

const merger = new PDFMerger();
(async () => {
	allPdfList.map((file) => merger.add(file.pdf, file.page || null));

	await merger.save(output);
	console.log("Merge complete and save " + output);
})();

Először is példányosítani kell a merger-t, majd egyesével hozzáadni a pdf fájlok nevét és a hozzá kapcsolódó oldalszámot, ha van hozzárendelve. Majd az await-el megvárjuk a mentést. Természetesen a végén adunk egy üzenetet, amivel jelezzük, hogy az új állomány elkészült.

Ezzel a programozás része is kész. Bármikor lehet tesztelni, ha az adott könyvtárban vagyunk, ahol a js állomány is van.

$ node index.js first.pdf -p 1-3 sec.pdf 10,15,20 out.pdf

Ha mindent jól csináltunk és a pdf elérhetősége is jó, akkor a programunk teszi a dolgát.

Innen már csak egy lépés maradt, hogy csináljunk belőle egy bárhonnan meghívható szoftvert. Ettől lesz teljes értékű Command Line Interface (CLI) alkalmazás.

Ennek a kivitelezésére két lehetőségünk van. Vagy az adott munkakönyvtárat használjuk úgynevezett link-eléssel. Ez nem más, mint a saját globális npm tárolóba létrehozunk egy a munka könyvtárra mutató linket és ezt használjuk mint csomag.

Ezt úgy tehetjük meg, hogy bemegyünk a munkakönyvtárba és kiadjuk a link parancsot:

$ npm link

Ha nem vagyunk helyileg ott, akkor a könyvtárat is meg kell adni a link parancs végére:

$ npm link /example/my-pdf-merge

Ennek a módszernek az az előnye, hogy ha módosítunk valamit a forráson, akkor azonnal meg is jelenik a CLI következő futása során. Ez teszteléshez nagyon jó megoldás.

A másik megoldás, hogy létrehozzunk egy npm csomagot és azt telepítjük fel simán az npm install-al. Ehhez a legegyszerűbb a munka könyvtárból kiadni a pack parancsot

$ npm pack

Ekkor létre jön egy tar.gz állomány, ami mindent tartalmaz, ami a telepítéshez és futtatáshoz szükséges.

Majd ezt a csomagot telepíteni kell, mint globális csomag, ehhez a -g kapcsolót használjuk:

$ npm install -g mypdf-merger.tar.gz

Ezzel a módszerrel feltelepül a lokális csomagunk úgy mintha egy rendes node csomag lenne. Ez sokkal kompaktabb és akár még küldhető forma is. Előnye, hogy nagyon egyszerű ezután feltenni a saját npm tárolóba és akkor mindig elérhető lesz számunkra online is. Az egyetlen hátránya, hogy minden változás után újra kell csomagolni, de oda kell figyelni a verzió szám növelésére is. Majd a telepítésnél az új verziót kell mindig megadni.

Én ezt a teljes szoftvert feltettem a GitHub tárolóba és az npm csomagot is bárki számára elérhető formában feltettem az npmjs.com-ra, mind a két helyen részletes használati leírással.

https://github.com/dovicsin/pdfmerge-cli

https://www.npmjs.com/package/@dovicsin/pdfmerge-cli