Tehnički članak

Kolacionisanje duplex skenova u Delphi: PDF interleave merge

CollateDocumentsEx u PDFlibPas Delphi PDF biblioteci spaja više otvorenih dokumenata u jedan preplet dokument. Za svaki krug dodaje GroupSize stranica iz svakog izvora, prihvata listu opsega stranica po izvoru, i tretira opadajući opseg poput 3-1 kao obrtanje tog izvora. Jedan poziv pretvara prednji stek i obrnuti zadnji stek u pravilan redosled čitanja

Scenario iza tog API-ja je banalan i izuzetno čest. Skener sa jednostranim prolazom vuče ceo stek okrenut naopako, a zatim operater okrene stek i pusti ga ponovo. Na kraju dobijate dva PDF-a: prednje strane po redu, zadnje strane u obrnutom redosledu. Korisnik želi jedan fajl, stranica 1 spreda, stranica 1 pozadi, stranica 2 spreda, i tako dalje. Ovaj članak se bavi problemom redosleda i zamkom dupliranja resursa koja se krije ispod njega. Ako vas zanima sirova propusnost konkatenacije, pogledajte brzo spajanje PDF-a pomeranjem referenci na bajt nivou; ako su ulazi previše veliki da stanu u memoriju, pogledajte spajanje i deljenje gigabajtnih PDF-ova sa direktnim pristupom

Skener proizvodi dva steka, jedan od njih unazad

Kolacionisanje nije spajanje. Merge nadovezuje opsege stranica; kolacionisanje ih preplitanjem kombinuje, a šablon preplitanja je svojstvo fizičkog uređaja koji je proizveo ulaz. Pogrešite šablon i fajl nije malo pogrešan, već je nečitljiv: svaka druga stranica pripada drugom listu. Tri promenljive opisuju skoro svaki realan slučaj: koliko izvora učestvuje u rotaciji, koliko stranica dolazi iz svakog izvora po krugu, i da li neki izvor treba čitati unazad. CollateDocuments pokriva prve dve pomoću običnog niza handle-ova dokumenata i celog broja GroupSize. CollateDocumentsEx dodaje treću tako što prihvata listu opsega stranica razdvojenih tačka-zapetom, po jedan segment za svaki izvor, gde prazan segment znači sve stranice tog izvora, a opadajući opseg ga obrće. Obe funkcije nadovezuju na kraj trenutno selektovanog dokumenta i vraćaju 1 pri uspehu, 0 pri bilo kom odbijanju

Zašto naivno kolacionisanje umnožava veličinu fajla?

Zato što se mapa importa koja mapira brojeve objekata izvora na brojeve objekata cilja iznova gradi pri svakom pozivu kopiranja, i sve što je dostupno iz više od jednog dela se uvozi po jednom uvozu za svaki deo. Unutar PDFlibPas, TPDFDocument.CopyPagesFromDoc resetuje svoju NewIndObjList na vrhu svakog poziva. Ta lista je jedino pamćenje koje kopir ima o tome šta je već preneo. Pozovite je jednom sa opsegom od deset stranica i font deljen na svih deset stranica biva ugrađen jednom. Pozovite je deset puta sa po jednom stranicom i isti taj font biva ugrađen deset puta. Ovo je mnogo bitnije za skenove nego za tekstualne dokumente, jer je skenirana stranica jedan veliki slikovni XObject, a deljeni objekti su upravo oni sa realnom težinom: ugrađeni ICC profil, deljeni /DecodeParms lanac, forma XObject sa žigom ili vodenim žigom primenjena na svaki list, font OCR tekstualnog sloja. Očigledan način pisanja round-robin kolacionisanja je petlja preko krugova, i ta petlja je tačno patološki slučaj

// 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;

Dvanaest krugova, dva izvora, dvadeset četiri mape importa. Ništa vas ne upozorava. Redosled stranica je ispravan, svaka stranica se renderuje, a jedini simptom je fajl nekoliko puta veći od zbira ulaza. Kod serijskog posla od 300 stranica taj multiplikator nije greška zaokruživanja, već razlika između arhive koja stane u budžet retencije i one koja ne stane

Uvezi jednom, pa preuredi stablo stranica

Ispravka je odvajanje dve brige koje je naivna petlja spojila u jednu. Kopiranje odlučuje koji objekti postoje u cilju; redosled odlučuje gde se stranice nalaze u stablu stranica. CollateDocumentsEx kopira svaki izvor tačno jednom, u jednom pozivu CopyPagesFromDoc sa kompletnim opsegom tog izvora, tako da svaki izvor dobija jednu mapu importa i deljeni resursi se pišu jednom. Tek pošto svaki izvor sleti, preplitanje se dešava, i dešava se u potpunosti preko TPDFPageTree.MovePage

Pomeranje stranica je besplatno u smislu koji je ovde bitan. ISO 32000-1 §7.7.3 definiše stablo stranica kao uravnoteženu strukturu čvorova-rečnika čiji /Kids nizovi sadrže indirektne reference, a /Count nosi ukupan broj listova na svakom čvoru. Premeštanje stranice znači uklanjanje jedne indirektne reference iz jednog /Kids niza, ubacivanje u drugi, podešavanje obe /Count vrednosti i ponovno usmeravanje /Parent stranice. Nijedan tok sadržaja se ne dira, nijedan resurs se ne duplira, nijedan objekat se ne pravi. Objekat stranice zadržava svoj broj objekta, što je i razlog zašto brojevi objekata ostaju stabilni na isti način kao u zameni stranica koja čuva brojeve objekata. Postoji još jedan detalj koji naivno pomeranje stranice pogrešno tretira, a MovePage ne. ISO 32000-1 §7.7.3.4 dozvoljava da /Resources, /MediaBox, /CropBox i /Rotate budu nasleđeni od pretka umesto navedeni na samoj stranici. Stranica koja nasleđuje svoje resurse od čvora A, a zatim se pomeri pod čvor B, tiho nasleđuje nešto drugačije, ili ništa. MovePage zato razrešava nasleđenu vrednost i upisuje je u rečnik stranice pre premeštanja, tako da stranica nosi sopstvene atribute kroz premeštanje

Šta prolaz preuređenja zapravo radi?

Pokreće selection sort naspram semantike ubacivanja-na-mesto. Željeni redosled relativan blok se računa prvo: obiđi izvore u rotaciji, uzmi do GroupSize indeksa iz svakog, preskoči izvor koji je iscrpljen, ponavljaj dok svaka stranica ne bude postavljena. To daje permutaciju nad dodatim blokom. Primena je nezgodan deo, jer je MovePage ubacivanje, a ne zamena, tako da svako pomeranje pomera sve između stare i nove pozicije za jedan

Implementacija drži niz Current koji modeluje gde se svaka dodata stranica trenutno nalazi, skenira unapred od pozicije K tražeći stranicu koja pripada na K, izdaje pomeranje, a zatim klizi unose niza da odraze šta je pomeranje uradilo stablu. To je O(n na kvadrat) u operacijama nad nizom i nula u kopijama objekata, što je ispravan kompromis za ovaj tip posla: kolacionisanje od 500 stranica je četvrt miliona premeštanja celih brojeva i nijedan bajt dupliranih slikovnih podataka. Opadajući opsezi i ponovljene stranice ne traže poseban tretman u ovom prolazu jer se PLParsePageRangeList poziva sa sortiranjem isključenim i duplikatima dozvoljenim, tako da traženi redosled preživi parsiranje netaknuto

Obrnuti opsezi i duplex spajanje u jednom pozivu

Kada je obrtanje izraženo kao opseg, slučaj dvostrukog prolaza kroz flatbed skener se svodi na jedan poziv. Prednje strane žele svoj prirodan redosled, a zadnje strane žele 12-1, a prazan prvi segment pre tačka-zapete kaže da prvi izvor doprinosi svim svojim stranicama

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;

Vredi eksplicitno navesti dva ponašanja iz tog isečka. Kolacionisane stranice se nadovezuju na selektovani dokument, tako da dokument napravljen sa NewDocument doprinosi svoju početnu praznu stranicu ispred njih, i treba je obrisati ako je ne želite. A izvori mogu biti neujednačeni: sa GroupSize 2 preko izvora od tri i pet stranica, krugovi ispadaju A1 A2 B1 B2, zatim A3 B3 B4 kad je A skoro potrošen, pa B5 samostalno, jer se iscrpljen izvor jednostavno preskače umesto da se popunjava

Rollback, polja formulara i šta ne prolazi

Svaki argument se validira pre nego što se cilj dodirne. Handle nedostajućeg dokumenta, selektovani dokument naveden kao sopstveni izvor, GroupSize manji od jedan, broj segmenata koji se ne poklapa sa brojem izvora, opseg koji imenuje stranicu koju izvor nema: sve to vraća 0 sa nepromenjenim ciljem. Neuspeh tokom kopiranja je teži slučaj, i obrađuje se preko javnog DeletePages, a ne sirovog PageTree.DeletePages. Razlog je konkretan. Kopiranje se izvodi sa uključenim MergeFormData, tako da su polja formulara izvora već dodata u /AcroForm /Fields niz cilja u trenutku kada kasniji izvor otkaže. Brisanje stranica na nivou stabla stranica bi uklonilo widget stranice i ostavilo te reference polja da vise; javna putanja odvezuje referencu polja, obrisa i lanca artikala uz stranice

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]));

Budite iskreni prema korisnicima o granicama. Kolacionisanje prenosi stranice, njihove anotacije i njihova polja formulara, i spaja listu polja AcroForm, niz redosleda izračunavanja i rečnik podrazumevanih resursa. Ne prenosi bookmark-e izvora: stablo obrisa skeniranog prednjeg steka je gotovo uvek prazno, tako da se ništa ne gubi u duplex slučaju, ali ako kolacionišete dva autorska dokumenta, njihovi obrisi ostaju izvan i navigaciju morate ponovo napraviti sami. Imenovane destinacije koje su živele samo u katalogu izvora su u istoj poziciji. Isplanirajte to pre nego što klijentu obećate bezgubitno kolacionisanje

PDFlibPas isporučuje funkcije kolacionisanja zajedno sa ostatkom svoje površine za sklapanje stranica, tako da radni tok skenera, ekstrakcija zasnovana na opsegu i putanje za velike fajlove sede iza jedne komponente u Delphi i C++Builder. Kompletna API referenca i probna verzija nalaze se na stranici proizvoda losLab Delphi PDF library