Odborný článok

Kolácia duplexných skenov v Delphi: PDF prekladanie

CollateDocumentsEx v Delphi PDF knižnici PDFlibPas zlučuje niekoľko otvorených dokumentov do jedného preloženého dokumentu. V každom kole pripojí GroupSize strán z každého zdroja, prijíma zoznam rozsahov strán pre jednotlivé zdroje a zostupný rozsah ako 3-1 chápe ako obrátenie daného zdroja. Jedno volanie premení hromadu predných strán a obrátenú hromadu zadných strán na čitateľné poradie

Scenár za týmto API je banálny a mimoriadne bežný. Podávací skener s jednostrannou dráhou prejde celú hromadu lícom nadol, potom operátor hromadu otočí a prežene ju znova. Výsledkom sú dva PDF súbory: predné strany v poradí, zadné strany v opačnom poradí. Používateľ chce jeden súbor: strana 1 predná, strana 1 zadná, strana 2 predná a tak ďalej. Tento článok sa venuje problému poradia a pasci duplikovania zdrojov, ktorá sa pod ním skrýva. Ak vás zaujíma čistá priepustnosť pri zreťazení, pozrite si rýchle zlučovanie PDF posunom referencií na úrovni bajtov; ak sú vstupy príliš veľké na to, aby sa vôbec zmestili do pamäte, pozrite si zlučovanie a delenie gigabajtových PDF s priamym prístupom

Skener produkuje dve hromady, jednu z nich naopak

Kolácia nie je zlučovanie. Zlučovanie zreťazuje rozsahy strán; kolácia ich prekladá, a vzorec prekladania je vlastnosťou fyzického zariadenia, ktoré vstup vyprodukovalo. Ak vzorec pokazíte, súbor nie je len mierne chybný, je nečitateľný: každá druhá strana patrí k inému hárku. Takmer každý reálny prípad opisujú tri premenné: koľko zdrojov je v rotácii, koľko strán prichádza z každého zdroja za kolo a či treba niektorý zdroj čítať odzadu. CollateDocuments pokrýva prvé dve pomocou obyčajného poľa handle-ov dokumentov a celočíselnej hodnoty GroupSize. CollateDocumentsEx pridáva tretiu tým, že prijíma bodkočiarkou oddelený zoznam rozsahov strán, jeden segment na zdroj, kde prázdny segment znamená všetky strany daného zdroja a zostupný rozsah ho obráti. Obe funkcie pripájajú na koniec aktuálne vybraného dokumentu a pri úspechu vracajú 1, pri akomkoľvek zamietnutí 0

Prečo naivná kolácia znásobuje veľkosť súboru?

Pretože mapa importu, ktorá mapuje čísla zdrojových objektov na čísla cieľových objektov, sa pri každom kopírovacom volaní znovu zostavuje, a čokoľvek dosiahnuteľné z viac než jedného úseku sa importuje raz za každý úsek. Vo vnútri PDFlibPas TPDFDocument.CopyPagesFromDoc resetuje svoj NewIndObjList na začiatku každého volania. Tento zoznam je jediná pamäť, ktorú kopírovací mechanizmus má o tom, čo už preniesol. Zavolajte ho raz s desaťstranovým rozsahom a font zdieľaný všetkými desiatimi stranami sa vloží raz. Zavolajte ho desaťkrát s jednou stranou zakaždým a ten istý font sa vloží desaťkrát. Toto má oveľa väčší vplyv na skeny než na textové dokumenty, pretože skenovaná strana je jeden veľký obrazový XObject a zdieľané objekty sú tie so skutočnou váhou: vložený ICC profil, zdieľaný reťazec /DecodeParms, formulárový XObject s pečiatkou alebo vodoznakom aplikovaný na každý hárok, font OCR textovej vrstvy. Zjavný spôsob, ako napísať kruhovú koláciu, je slučka cez kolá, a táto slučka je presne ten patologický prípad

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

Dvanásť kôl, dva zdroje, dvadsaťštyri máp importu. Nič vás nevaruje. Poradie strán je správne, každá strana sa vykreslí, a jediným príznakom je súbor niekoľkonásobne väčší než súčet svojich vstupov. Pri dávkovej úlohe s 300 stranami nie je násobiteľ zaokrúhľovacia chyba, je to rozdiel medzi archívom, ktorý sa zmestí do rozpočtu na uchovávanie, a takým, ktorý sa nezmestí

Importujte raz, potom preusporiadajte strom strán

Riešením je oddeliť dve záležitosti, ktoré naivná slučka spojila do jednej. Kopírovanie rozhoduje, ktoré objekty existujú v cieli; usporiadanie rozhoduje, kde strany sedia v strome strán. CollateDocumentsEx kopíruje každý zdroj presne raz, jediným volaním CopyPagesFromDoc s kompletným rozsahom daného zdroja, takže každý zdroj dostane jednu mapu importu a zdieľané zdroje sa zapíšu raz. Až keď je každý zdroj na mieste, dôjde k prekladaniu, a to výhradne prostredníctvom TPDFPageTree.MovePage

Presuny strán sú v zmysle, ktorý tu záleží, zadarmo. ISO 32000-1 §7.7.3 definuje strom strán ako vyváženú štruktúru uzlových slovníkov, ktorých polia /Kids obsahujú nepriame referencie, pričom /Count nesie celkový počet listov v každom uzle. Presun strany znamená odstránenie jednej nepriamej referencie z jedného poľa /Kids, jej vloženie do iného, úpravu oboch hodnôt /Count a prepojenie /Parent strany na nový uzol. Nedotkne sa žiadny obsahový stream, neduplikuje sa žiadny zdroj, nevytvorí sa žiadny objekt. Objekt strany si zachováva svoje číslo objektu, čo je aj dôvod, prečo čísla objektov zostávajú stabilné rovnako ako v nahradzovaní strán so zachovaním čísel objektov. Existuje ešte jeden detail, ktorý naivný presun strany pokazí a MovePage nie. ISO 32000-1 §7.7.3.4 umožňuje, aby /Resources, /MediaBox, /CropBox a /Rotate boli zdedené od predkovho uzla namiesto toho, aby boli uvedené priamo na strane. Strana, ktorá dedí svoje zdroje z uzla A a potom sa presunie pod uzol B, ticho zdedí niečo iné, alebo vôbec nič. MovePage preto pred presunom vyrieši zdedenú hodnotu a zapíše ju do slovníka strany, takže strana si svoje vlastné atribúty nesie so sebou aj cez presun

Čo presne robí prechod preusporiadania?

Beží ako výberové triedenie voči sémantike „vlož na pozíciu“. Najprv sa vypočíta požadované poradie relatívne k bloku: prejdite zdroje v rotácii, vezmite z každého až GroupSize indexov, preskočte zdroj, ktorý je vyčerpaný, opakujte, kým nie je umiestnená každá strana. Tým vznikne permutácia nad pripojeným blokom. Jej aplikácia je tá nepríjemná časť, pretože MovePage je vloženie, nie zámena, takže každý presun posunie o jedno miesto všetko medzi starou a novou pozíciou

Implementácia udržiava pole Current, ktoré modeluje, kde práve sedí každá pripojená strana, prehľadáva dopredu od pozície K stranu, ktorá tam patrí, vykoná presun, potom posunie záznamy poľa tak, aby odzrkadľovali, čo presun urobil so stromom. Ide o O(n na druhú) v operáciách s poľom a nulu v kopírovaní objektov, čo je pre túto úlohu správny kompromis: kolácia s 500 stranami je štvrť milióna celočíselných presunov a ani jeden bajt duplikovaných obrazových dát. Zostupné rozsahy a opakované strany nevyžadujú v tomto prechode žiadne osobitné zaobchádzanie, pretože PLParsePageRangeList sa volá s vypnutým triedením a povolenými duplicitami, takže požadované poradie prežije parsovanie nedotknuté

Obrátené rozsahy a jednovolaniové duplexné zlúčenie

Keď je obrátenie vyjadrené ako rozsah, prípad dvojprechodového plochého skenera sa zredukuje na jediné volanie. Predné strany chcú svoje prirodzené poradie a zadné strany chcú 12-1, a prázdny prvý segment pred bodkočiarkou hovorí, že prvý zdroj prispieva všetkými svojimi stranami

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;

Stojí za to výslovne spomenúť dve správania v tomto úryvku. Zlúčené strany sa pripájajú k vybranému dokumentu, takže dokument vytvorený pomocou NewDocument pred ne prispeje svojou počiatočnou prázdnou stranou, a ak ju nechcete, musíte ju odstrániť. A zdroje môžu byť nerovnomerné: pri GroupSize 2 nad trojstranovým a päťstranovým zdrojom vyjdú kolá A1 A2 B1 B2, potom A3 B3 B4, keď je A takmer vyčerpaný, potom samotné B5, pretože vyčerpaný zdroj sa jednoducho preskočí, nedopĺňa sa

Rollback, formulárové polia a čo sa nepreberie

Každý argument sa validuje skôr, než sa cieľ vôbec dotkne. Chýbajúci handle dokumentu, vybraný dokument uvedený ako vlastný zdroj, GroupSize pod jedna, počet segmentov nezodpovedajúci počtu zdrojov, rozsah pomenujúci stranu, ktorú zdroj nemá: to všetko vráti 0 a cieľ zostáva nezmenený. Zlyhanie počas kopírovania je náročnejší prípad a rieši sa cez verejnú DeletePages namiesto surovej PageTree.DeletePages. Dôvod je konkrétny. Kopírovanie beží s povoleným MergeFormData, takže formulárové polia zdroja už boli pripojené do poľa /AcroForm /Fields cieľa v momente, keď neskorší zdroj zlyhá. Odstránenie strán na úrovni stromu strán by odstránilo widget strany a nechalo by tieto referencie polí visieť naprázdno; verejná cesta odpojí referenciu poľa, osnovy aj vlákien článkov spolu so stranami

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

Buďte voči svojim používateľom úprimní ohľadom hraníc. Kolácia prenáša strany, ich anotácie a ich formulárové polia, a zlučuje zoznam polí AcroForm, pole poradia výpočtu a slovník predvolených zdrojov. Neprenáša zdrojové záložky: strom osnovy skenovanej hromady predných strán je takmer vždy prázdny, takže v duplexnom prípade sa nič nestratí, ale ak zlučujete dva autorsky vytvorené dokumenty, ich osnovy zostanú vzadu a navigáciu si musíte znovu zostaviť sami. Pomenované ciele, ktoré žili iba v zdrojovom katalógu, sú v rovnakej situácii. Naplánujte si to skôr, než zákazníkovi sľúbite bezstratovú koláciu

PDFlibPas dodáva funkcie kolácie spolu so zvyškom svojej plochy pre skladanie strán, takže workflow skenera, extrakcia podľa rozsahu aj cesty pre veľké súbory sedia za jednou komponentou v Delphi a C++Builder. Úplná referencia API a skúšobný build sú na stránke produktu losLab Delphi PDF library