A CollateDocumentsEx a PDFlibPas Delphi PDF-könyvtárban több megnyitott dokumentumot fésül össze egyetlen, egymásba fűzött dokumentummá. Fordulónként GroupSize oldalt fűz hozzá minden egyes forrásból, forrásonkénti oldaltartomány-listát fogad el, és a csökkenő tartományt, például 3-1, az adott forrás megfordításaként kezeli. Egyetlen hívással egy előlapi köteg és egy fordított hátlapi köteg olvasási sorrendbe rendeződik
Az API mögötti forgatókönyv hétköznapi és rendkívül gyakori. Egy egyoldalas útvonalú, íves adagolású szkenner az egész köteget lefelé fordítva futtatja végig, majd a kezelő megfordítja a köteget és újra átfuttatja. Ennek eredménye két PDF: az előlapok sorrendben, a hátlapok fordított sorrendben. A felhasználó egyetlen fájlt szeretne, ahol az 1. oldal előlapja, az 1. oldal hátlapja, a 2. oldal előlapja következik így tovább. Ez a cikk a sorrendezési problémáról szól, valamint az alatta megbúvó erőforrás-duplikációs csapdáról. Ha a nyers összefűzés átviteli sebessége érdekli inkább, lásd a gyors PDF-összefésülést bájtszintű hivatkozás-eltolással; ha a bemenetek túl nagyok ahhoz, hogy egyáltalán memóriában elférjenek, lásd a gigabájtos PDF-ek összefésülését és felosztását közvetlen hozzáféréssel
A szkenner két köteget készít, az egyik fordítva van
A collation nem ugyanaz, mint az összefésülés. Az összefésülés oldaltartományokat fűz össze; a collation egymásba illeszti azokat, és az illesztési minta a bemenetet előállító fizikai eszköz tulajdonsága. Ha a minta hibás, a fájl nem kicsit rossz, hanem olvashatatlan: minden második oldal egy másik laphoz tartozik. Három változó írja le szinte az összes valós esetet: hány forrás vesz részt a körforgásban, fordulónként hány oldal érkezik az egyes forrásokból, és kell-e valamelyik forrást visszafelé olvasni. A CollateDocuments az első kettőt fedi le dokumentumkezelők egyszerű tömbjével és egy GroupSize egésszel. A CollateDocumentsEx a harmadikat is hozzáadja, mivel pontosvesszővel elválasztott oldaltartomány-listát fogad el, forrásonként egy szegmenssel, ahol egy üres szegmens az adott forrás összes oldalát jelenti, egy csökkenő tartomány pedig megfordítja azt. Mindkét függvény a jelenleg kiválasztott dokumentum végéhez fűz hozzá, és sikeres futás esetén 1-et, elutasítás esetén 0-t ad vissza
Miért szorozza meg a naiv collation a fájlméretet?
Mert az importleképezés, amely a forrás objektumszámokat a cél objektumszámaira képezi le, minden másolási hívásnál újraépül, és minden, ami egynél több szeletből elérhető, szeletenként egyszer kerül importálásra. A PDFlibPas belsejében a TPDFDocument.CopyPagesFromDoc minden meghívásának elején visszaállítja a NewIndObjList listáját. Ez az egyetlen memória, amellyel a másoló rendelkezik arról, hogy mit hozott már át korábban. Ha egyszer hívjuk meg tízoldalas tartománnyal, egy mind a tíz oldal által megosztott betűtípus egyszer kerül beágyazásra. Ha tízszer hívjuk meg egy-egy oldallal, ugyanaz a betűtípus tízszer kerül beágyazásra. Ez sokkal jobban számít szkennelt anyagoknál, mint szöveges dokumentumoknál, mert egy szkennelt oldal egyetlen nagy kép XObject, és a megosztott objektumok azok, amelyek valódi súllyal bírnak: egy beágyazott ICC-profil, egy megosztott /DecodeParms lánc, minden lapra alkalmazott bélyegző vagy vízjel form XObject, az OCR szövegréteg betűtípusa. A körforgásos collation megírásának kézenfekvő módja egy fordulókon végigmenő ciklus, és ez a ciklus pontosan a patologikus eset
// Do not do this. Each CopyPageRanges call rebuilds the import map,
// so anything the two sources share internally is imported once per
// round instead of once per source.
var
RoundIndex: Integer;
begin
for RoundIndex := 1 to 12 do
begin
PDF.CopyPageRanges(Fronts, IntToStr(RoundIndex));
PDF.CopyPageRanges(Backs, IntToStr(13 - RoundIndex));
end;
end;
Tizenkét forduló, két forrás, huszonnégy importleképezés. Semmi nem figyelmeztet erre. Az oldalsorrend helyes, minden oldal helyesen jelenik meg, és az egyetlen tünet az, hogy a fájl a bemenetek összegének többszöröse. Egy 300 oldalas kötegelt feladatnál a szorzó nem kerekítési hiba, hanem a különbség egy megőrzési költségkeretbe beleférő és egy abba bele nem férő archívum között
Egyszer importálj, aztán rendezd újra az oldalfát
A megoldás abban áll, hogy szétválasztjuk a naiv ciklusban összeolvadt két szempontot. A másolás dönti el, mely objektumok léteznek a célban; a sorrendezés dönti el, hol helyezkednek el az oldalak az oldalfában. A CollateDocumentsEx minden forrást pontosan egyszer másol, egyetlen CopyPagesFromDoc hívással, amely az adott forrás teljes tartományát lefedi, így minden forráshoz egy importleképezés tartozik, és a megosztott erőforrások egyszer kerülnek felírásra. Csak azután, hogy minden forrás megérkezett, történik meg az egymásba illesztés, és ez teljes egészében a TPDFPageTree.MovePage révén zajlik
Az oldalmozgatások ebben az összefüggésben ingyenesek. Az ISO 32000-1 §7.7.3 az oldalfát csomópont-szótárak kiegyensúlyozott struktúrájaként definiálja, ahol a /Kids tömbök közvetett hivatkozásokat tartalmaznak, a /Count pedig minden csomópontnál a levelek összesített számát hordozza. Egy oldal áthelyezése azt jelenti, hogy egy közvetett hivatkozást eltávolítunk az egyik /Kids tömbből, beillesztjük egy másikba, mindkét /Count értéket módosítjuk, és átirányítjuk az oldal /Parent mutatóját. Egyetlen tartalomfolyamhoz sem nyúlunk, egyetlen erőforrás sem duplikálódik, egyetlen objektum sem jön létre. Az oldalobjektum megtartja az objektumszámát, ez az oka annak is, hogy az objektumszámok ugyanúgy stabilak maradnak, mint az objektumszámokat megőrző oldalcserénél. Van még egy részlet, amelyet egy naiv oldalmozgatás elront, a MovePage viszont nem. Az ISO 32000-1 §7.7.3.4 megengedi, hogy a /Resources, a /MediaBox, a /CropBox és a /Rotate egy ős csomóponttól öröklődjön ahelyett, hogy magán az oldalon szerepelne. Egy olyan oldal, amely az A csomóponttól örökli az erőforrásait, majd a B csomópont alá kerül áthelyezésre, csendben valami mást örököl, vagy egyáltalán semmit. A MovePage ezért az áthelyezés előtt feloldja az öröklött értéket, és felírja azt az oldalszótárra, így az oldal az áthelyezés során is megtartja saját attribútumait
Mit csinál valójában az újrarendezési lépés?
Egy kiválasztásos rendezést futtat beszúrás-alapú szemantikával szemben. Először a blokkon belüli kívánt sorrendet számítja ki: végigmegy a forrásokon a körforgás szerint, legfeljebb GroupSize indexet vesz mindegyikből, kihagyja a kimerült forrást, és ezt ismétli, amíg minden oldal elhelyezésre nem kerül. Ez egy permutációt eredményez a hozzáfűzött blokk fölött. Ennek alkalmazása a nehezebb rész, mert a MovePage beszúrás, nem csere, így minden mozgatás egy pozícióval eltolja a régi és az új pozíció közötti összes elemet
A megvalósítás egy Current tömböt tart karban, amely modellezi, hogy az egyes hozzáfűzött oldalak jelenleg hol helyezkednek el, K pozíciótól előre haladva keresi meg azt az oldalt, amelynek K-nál kell lennie, kiadja a mozgatást, majd eltolja a tömb bejegyzéseit, hogy tükrözze, mit tett a mozgatás a fával. Ez O(n négyzet) tömbműveletben, és nulla objektummásolásban, ami helyes csere ehhez a munkaterheléshez: egy 500 oldalas collation negyedmillió egész szám átrendezését jelenti, és egyetlen bájtnyi duplikált képadatot sem. A csökkenő tartományoknak és az ismételt oldalaknak nincs szükségük speciális kezelésre ebben a lépésben, mert a PLParsePageRangeList kikapcsolt rendezéssel és engedélyezett duplikátumokkal kerül meghívásra, így a kért sorrend sértetlenül megmarad az elemzés után is
Fordított tartományok és az egyhívásos duplex összefésülés
Ha a megfordítás tartományként fejeződik ki, a síkágyas kétmenetes eset egyetlen hívásra egyszerűsödik. Az előlapok a természetes sorrendjüket szeretnék, a hátlapok pedig a 12-1 tartományt, és a pontosvessző előtti üres első szegmens azt jelenti, hogy az első forrás az összes oldalát hozzáadja
var
PDF: TPDFlib;
Target, Fronts, Backs: Integer;
begin
PDF := TPDFlib.Create;
try
Target := PDF.NewDocument;
if PDF.LoadFromFile('fronts.pdf', '') <> 1 then
Exit;
Fronts := PDF.SelectedDocument;
if PDF.LoadFromFile('backs.pdf', '') <> 1 then
Exit;
Backs := PDF.SelectedDocument;
PDF.SelectDocument(Target);
// fronts 1..12 in order, backs scanned in reverse: F1 B12 F2 B11 ...
if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 1 then
PDF.SaveToFile('duplex.pdf');
finally
PDF.Free;
end;
end;
Ebben a kódrészletben két viselkedést érdemes kifejezetten megemlíteni. A collation eredményeként kapott oldalak a kiválasztott dokumentumhoz kerülnek hozzáfűzésre, így egy NewDocument segítségével létrehozott dokumentum az induláskori üres oldalával előzi meg azokat, és ezt törölni kell, ha nem szeretnénk megtartani. A források emellett egyenetlenek is lehetnek: 2-es GroupSize mellett egy háromoldalas és egy ötoldalas forrás esetén a fordulók A1 A2 B1 B2, majd A3 B3 B4, mihelyt az A forrás majdnem kimerül, végül B5 önmagában, mert egy kimerült forrást egyszerűen kihagyunk, nem pedig kitöltünk
Visszaállítás, űrlapmezők, és ami nem kerül át
Minden argumentum ellenőrzésre kerül, mielőtt a cél bármilyen módosítást kapna. Egy hiányzó dokumentumkezelő, a kiválasztott dokumentum saját forrásaként való felsorolása, egy egynél kisebb GroupSize, egy olyan szegmensszám, amely nem egyezik a forrásszámmal, egy olyan tartomány, amely a forrásban nem létező oldalra hivatkozik: mindegyik esetben 0 értékkel tér vissza, a cél pedig változatlan marad. A másolás közbeni hiba a nehezebb eset, és ezt a nyilvános DeletePages kezeli, nem a nyers PageTree.DeletePages. Ennek konkrét oka van. A másolás bekapcsolt MergeFormData mellett fut, így a forrás űrlapmezői már hozzá lettek fűzve a cél /AcroForm /Fields tömbjéhez, mire egy későbbi forrás meghiúsul. Az oldalak eltávolítása az oldalfa szintjén lecsupaszítaná a widget oldalakat, és a mezőhivatkozásokat lógva hagyná; a nyilvános útvonal a mező-, körvonal- és cikkfonál-hivatkozásokat is leválasztja az oldalak mellett
if PDF.CollateDocumentsEx([Fronts, Backs], ';12-1', 1) = 0 then
// Nothing was appended and the target is byte-identical to before.
// 412 is the copy failure; 0 means the arguments were rejected
// during validation, before any page was touched.
Log(Format('collate rejected, LastErrorCode=%d', [PDF.LastErrorCode]));
Legyünk őszinték a felhasználóink felé a határokat illetően. A collation áthozza az oldalakat, azok jegyzeteit és űrlapmezőit, és összefésüli az AcroForm mezőlistát, a számítási sorrend tömböt és az alapértelmezett erőforrás-szótárat. Nem hozza át a forrás könyvjelzőit: egy szkennelt előlapi köteg körvonalfája szinte mindig üres, így a duplex esetben semmi nem vész el, de ha két szerkesztett dokumentumot fésülünk össze, azok körvonalai a helyükön maradnak, és a navigációt magunknak kell újraépítenünk. A csak a forráskatalógusban élő elnevezett célok ugyanebben a helyzetben vannak. Ezt tervezzük be, mielőtt veszteségmentes collationt ígérnénk egy ügyfélnek
A PDFlibPas a collation függvényeket az oldalösszeállítási felület többi részével együtt szállítja, így a szkennerworkflow, a tartomány alapú kinyerés és a nagyméretű fájlokra vonatkozó útvonalak mind egyetlen komponens mögött helyezkednek el Delphiben és C++Builderben. A teljes API-referencia és egy próbaverzió a losLab Delphi PDF library termékoldalon található