CollateDocumentsEx v Delphi PDF knjižnici PDFlibPas združi več odprtih dokumentov v en prepleten dokument. Iz vsakega vira v vsakem krogu doda GroupSize strani, sprejme seznam obsegov strani za posamezen vir in obravnava padajoč obseg, kot je 3-1, kot obrat tega vira. En sam klic spremeni sprednji sklad in obrnjen hrbtni sklad v pravilen bralni vrstni red
Scenarij za tem API-jem je vsakdanji in izjemno pogost. Enostranski listni optični bralnik prebere cel sklad z obrazom navzdol, nato operater sklad obrne in ga požene še enkrat. Rezultat sta dva PDF-ja: sprednje strani v vrstnem redu, hrbtne strani v obratnem vrstnem redu. Uporabnik želi eno samo datoteko: stran 1 sprednja, stran 1 hrbtna, stran 2 sprednja in tako naprej. Ta članek obravnava problem vrstnega reda in past podvajanja virov, ki je pod njim. Če vas zanima zgolj hitrost surovega spajanja, glejte hitro spajanje PDF-jev s premikanjem referenc na ravni bajtov; če vhodne datoteke sploh ne gredo v pomnilnik, glejte spajanje in delitev gigabajtnih PDF-jev z neposrednim dostopom
Optični bralnik ustvari dva sklada, enega obrnjenega
Zbiranje ni spajanje. Spajanje veriži obsege strani; zbiranje jih prepleta, vzorec prepletanja pa je lastnost fizične naprave, ki je vhod ustvarila. Če vzorec ni pravilen, datoteka ni malo napačna, temveč neuporabna: vsaka druga stran pripada drugemu listu. Skoraj vsak resničen primer opišejo tri spremenljivke: koliko virov je v rotaciji, koliko strani pride iz vsakega vira na krog in ali je treba kateri vir brati nazaj. CollateDocuments pokrije prvi dve s preprostim poljem ročnikov dokumentov in celim številom GroupSize. CollateDocumentsEx doda tretjo tako, da sprejme s podpičjem ločen seznam obsegov strani, en segment na vir, kjer prazen segment pomeni vse strani tega vira, padajoč obseg pa ga obrne. Obe funkciji dodajata na konec trenutno izbranega dokumenta in ob uspehu vrneta 1, ob vsaki zavrnitvi pa 0
Zakaj naivno zbiranje pomnoži velikost datoteke?
Ker se uvozna preslikava, ki preslika številke objektov vira v številke objektov cilja, obnovi ob vsakem klicu kopiranja, vse, kar je dosegljivo iz več kot enega kosa, pa se uvozi enkrat na kos. Znotraj PDFlibPas TPDFDocument.CopyPagesFromDoc ob vsakem klicu ponastavi svoj NewIndObjList. Ta seznam je edini spomin kopirnika na to, kaj je že prenesel. Kličite ga enkrat z obsegom desetih strani in pisava, skupna vseh desetih stranem, se vgradi enkrat. Kličite ga desetkrat, vsakič z eno stranjo, in ista pisava se vgradi desetkrat. To je za skenirane dokumente veliko pomembnejše kot za besedilne, saj je skenirana stran ena velika slikovna XObject, skupni objekti pa so tisti z resnično težo: vgrajen profil ICC, skupna veriga /DecodeParms, štampiljka ali vodni žig, uporabljen na vsak list, pisava plasti besedila iz OCR. Očiten način za pisanje krožnega zbiranja je zanka čez kroge, in prav ta zanka je patološki primer
// 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;
Dvanajst krogov, dva vira, štiriindvajset uvoznih preslikav. Nič ne opozori. Vrstni red strani je pravilen, vsaka stran se izriše, edini simptom pa je datoteka, ki je nekajkrat večja od vsote svojih vhodov. Pri paketnem opravilu s 300 stranmi ta množitelj ni napaka zaokroževanja, temveč razlika med arhivom, ki ustreza proračunu hrambe, in tistim, ki mu ne
Uvozi enkrat, nato preuredi drevo strani
Rešitev je ločiti dve skrbi, ki ju je naivna zanka združila. Kopiranje odloča, kateri objekti obstajajo v cilju; vrstni red odloča, kje strani sedijo v drevesu strani. CollateDocumentsEx kopira vsak vir natanko enkrat, v enem samem klicu CopyPagesFromDoc s celotnim obsegom tega vira, tako da vsak vir dobi eno uvozno preslikavo, skupni viri pa se zapišejo enkrat. Šele ko so vsi viri na mestu, se zgodi prepletanje, in to izključno prek TPDFPageTree.MovePage
Premiki strani so v tem smislu brezplačni. ISO 32000-1 §7.7.3 opredeljuje drevo strani kot uravnoteženo strukturo slovarjev vozlišč, katerih polja /Kids vsebujejo posredne reference, pri čemer /Count nosi skupno število listov na vsakem vozlišču. Premestitev strani pomeni odstranitev ene posredne reference iz enega polja /Kids, vstavitev v drugo, prilagoditev obeh vrednosti /Count in preusmeritev polja /Parent strani. Nobena vsebinska tok se ne dotakne, noben vir se ne podvoji, noben objekt se ne ustvari. Objekt strani obdrži svojo številko objekta, kar je tudi razlog, da številke objektov ostanejo stabilne, tako kot v zamenjavi strani, ki ohranja številke objektov. Obstaja še ena podrobnost, ki jo naiven premik strani zgreši, MovePage pa ne. ISO 32000-1 §7.7.3.4 dovoljuje, da se /Resources, /MediaBox, /CropBox in /Rotate podedujejo od prednika namesto da so navedeni na strani. Stran, ki podeduje vire od vozlišča A in se nato premakne pod vozlišče B, tiho podeduje nekaj drugega ali nič. MovePage zato razreši podedovano vrednost in jo zapiše v slovar strani pred premestitvijo, tako da stran ob premiku ohrani svoje lastne atribute
Kaj dejansko počne prehod preurejanja?
Izvede izbirno razvrščanje glede na semantiko vstavljanja. Želeni vrstni red, relativen na blok, se najprej izračuna: viri se obiskujejo v rotaciji, iz vsakega se vzame do GroupSize indeksov, izčrpan vir se preskoči, postopek se ponavlja, dokler ni vsaka stran razporejena. To ustvari permutacijo nad dodanim blokom. Njena uporaba je nerodni del, saj je MovePage vstavljanje, ne zamenjava, zato vsak premik za eno mesto premakne vse med staro in novo pozicijo
Implementacija hrani polje Current, ki modelira, kje trenutno sedi vsaka dodana stran, pregleda naprej od položaja K stran, ki tja spada, izvede premik, nato pa zamakne vnose polja, da zrcalijo, kaj je premik naredil drevesu. To je O(n na kvadrat) v operacijah nad poljem in nič v kopiranju objektov, kar je za to obremenitev pravilna zamenjava: zbiranje 500 strani je četrt milijona premikov celih števil in niti en bajt podvojenih slikovnih podatkov. Padajoči obsegi in ponovljene strani ne potrebujejo posebne obravnave v tem prehodu, ker se PLParsePageRangeList kliče z onemogočenim razvrščanjem in dovoljenimi podvojitvami, tako da zahtevani vrstni red preživi razčlenjevanje nedotaknjen
Obrnjeni obsegi in enoklicno dupleks spajanje
Ko je obrat izražen kot obseg, se primer dvojnega prehoda na ploskem optičnem bralniku strne v en sam klic. Sprednje strani hočejo svoj naravni vrstni red, hrbtne pa 12-1, prazen prvi segment pred podpičjem pa pove, da prvi vir prispeva vse svoje strani
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;
V tem izsečku sta vredni izrecne omembe dve obnašanji. Zbrane strani se dodajo izbranemu dokumentu, zato dokument, ustvarjen z NewDocument, pred njimi prispeva svojo začetno prazno stran, ki jo je treba izbrisati, če je ne želite. Viri so lahko tudi neenakomerni: pri GroupSize 2 nad virom s tremi in virom s petimi stranmi krogi potekajo kot A1 A2 B1 B2, nato A3 B3 B4, ko je A skoraj izčrpan, nato B5 sam, ker se izčrpan vir preprosto preskoči namesto da bi ga zapolnili
Povrnitev, polja obrazcev in kaj ne pride s seboj
Vsak argument se preveri, preden se cilj sploh dotakne. Manjkajoč ročnik dokumenta, izbrani dokument, naveden kot lasten vir, GroupSize pod ena, število segmentov, ki se ne ujema s številom virov, obseg, ki imenuje stran, ki je vir nima: vse to vrne 0 in cilj ostane nespremenjen. Napaka med kopiranjem je težji primer in se obravnava prek javnega DeletePages, ne prek surovega PageTree.DeletePages. Razlog je natančen. Kopiranje teče z omogočenim MergeFormData, zato so bila polja obrazca vira do trenutka, ko poznejši vir odpove, že dodana polju /AcroForm /Fields cilja. Brisanje strani na ravni drevesa strani bi odstranilo strani z gradniki in pustilo te reference na polja viseti; javna pot pa poleg strani odveže tudi reference na polja, oris in verige člankov
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]));
Bodite iskreni do svojih uporabnikov glede meja. Zbiranje prenese strani, njihove opombe in njihova polja obrazcev ter združi seznam polj AcroForm, polje vrstnega reda izračuna in slovar privzetih virov. Ne prenese izvornih zaznamkov: drevo orisa skeniranega sprednjega sklada je skoraj vedno prazno, zato pri dupleks primeru nič ni izgubljeno, če pa zbirate dva avtorska dokumenta, njuni orisi ostanejo zadaj in navigacijo morate obnoviti sami. Poimenovani cilji, ki so obstajali le v katalogu vira, so v enakem položaju. To predvidite, preden stranki obljubite brezizgubno zbiranje
PDFlibPas dostavlja funkcije zbiranja skupaj z ostalim naborom za sestavljanje strani, tako da so delovni tok optičnega bralnika, ekstrakcija na osnovi obsegov in poti za velike datoteke vse za eno komponento v Delphiju in C++Builderju. Celoten referenčni API in preizkusna različica sta na strani izdelka losLab Delphi PDF Library