Technical Article

N-up vyradenie stránok (imposition) a zmena poradia stránok s PDFium

Zlúčenie (merge) a rozdelenie (split) sú dve operácie so stránkami, po ktorých každý siahne ako po prvých, a pokrývajú veľa scenárov. Nepokrývajú však všetko. Existuje samostatná rodina prác, ktorá mení usporiadanie stránok namiesto presúvania celých súborov: umiestnenie štyroch snímok na jeden list pre podklady, pretiahnutie stránky zo zadu dokumentu dopredu alebo vytiahnutie stránok 3, 7 a 12 do krátkeho výseku bez ovplyvnenia zvyšku. PDFium odhaľuje tri metódy presne na toto a každá z nich sa správa inak ako zlúčenie a rozdelenie, ktoré už poznáte. Tento článok prechádza tým, čo robia, kde žijú výstupné body a jedným detailom vlastníctva, ktorý spôsobil pád v reálnej prevádzke.

Čo vlastne robí N-up vyradenie stránok

Imposition (vyradenie stránok) je tlačiarenský pojem pre usporiadanie niekoľkých zdrojových stránok na jeden väčší list tak, aby sa vytlačený a poskladaný výsledok čítal v správnom poradí. Každodennou verziou sú podklady 2-up, 4-up podpis brožúry alebo kontaktný hárok, ktorý zmestí tucet náhľadov na jednu stránku. PDFium zvláda geometriu prostredníctvom jedného volania:

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

Parametre NumX a NumY popisujú mriežku. Hodnota 2, 1 umiestni dve zdrojové stránky vedľa seba; 2, 2 zabalí štyri do štvrťového rozloženia; 4, 3 postaví dvanásť-stranový kontaktný hárok. PDFium číta zdrojové stránky v poradí, zmenšuje každú z nich tak, aby sa zmestila do svojej bunky, a vypĺňa mriežku zľava doprava, zhora nadol, pričom začne nový výstupný hárok vždy, keď je aktuálna mriežka plná. Zdrojové stránky sa nemenia. To, čo dostanete späť, je nový dokument, ktorého stránky sú kompozity.

Veľkosť výstupu je v bodoch, nie v pixeloch

Parametre OutputWidth a OutputHeight sú používateľské jednotky PDF, pričom používateľská jednotka PDF je jeden bod, čo je jedna sedemdesiatdvojina palca. Jednotka deklaruje fyzickú veľkosť výstupného hárku a nemá nič spoločné s pixelmi obrazovky alebo DPI vykresľovania. Toto je najčastejšie miesto, kde sa robí chyba pri vyradení stránok, pretože vývojár zvyknutý na bitmapy siahne po počte pixelov a skončí s hárkom veľkosti poštovej známky alebo billboardu.

Čísla, ktoré si stojí za to zapamätať, sú dve veľkosti stránok, ktoré budete používať najčastejšie. US Letter je 612 x 792 bodov, pretože 8,5 palca krát 72 je 612 a 11 palcov krát 72 je 792. A4 je približne 595 x 842 bodov, vychádzajúc z jej rozmerov 210 x 297 milimetrov. Hlavička samotného bindingu jasne uvádza pravidlo, že jedna jednotka je jedna sedemdesiatdvojina palca, a jednotka dodáva konštantu PointsPerInch rovnú 72, ak by ste radšej počítali veľkosť z palcov v kóde, než písali literál.

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

Vrátený handle musíte uvoľniť vy

Prečítajte si podpis znova. ImportNPagesToOne vracia TPdf, nie Boolean. Táto návratová hodnota je úplne nový handle dokumentu, alokovaný nezávisle od zdroja, a volajúci ho vlastní. Zdrojový TPdf, na ktorom ste metódu volali, je nedotknutý a stále vlastní svoj vlastný handle; kompozit je druhým, nezávislým objektom. Ak necháte vrátený TPdf odísť z rozsahu platnosti bez jeho uvoľnenia, dôjde k úniku (leak) celého dokumentu PDFium.

Ešte nebezpečnejšia chyba funguje opačne. Pod kapotou metóda žiada PDFium o nový FPDF_DOCUMENT cez FPDF_ImportNPagesToOne, a potom tento surový handle obalí do vráteného TPdf, takže životnosť obalu riadi životnosť handlu. Od tohto momentu existuje presne jedna osoba vlastniaca handle a presne jedno miesto, kde by mal byť zatvorený: keď uvoľníte (Free) vrátený objekt. Nedbalá chybová cesta, ktorá uvoľní obal a zároveň zavolá FPDF_CloseDocument na surovom handle, ktorý zachytila, zatvorí ten istý PDFium dokument dvakrát. Ide o dvojité uvoľnenie (double-free), a to je tá konkrétna chyba, ktorá tu kdysi potrápila volajúceho. Pravidlo, ktoré tomu predchádza, je krátke. Zatvorte dokument iba na jednej ceste uvoľnením TPdf, ktoré vám metóda odovzdala, a nikdy nesiahajte za obal na zatvorenie handlu, ktorý už adoptoval.

Z tohto vyvstávajú dva dôsledky. Po prvé, metóda vracia nil, keď PDFium odmietne argumenty, ako napríklad nulu na ktorejkoľvek osi mriežky alebo zlyhanie alokácie, takže kontrola na nil patrí pred to, než sa dotknete výsledku. Po druhé, inicializujte svoju výstupnú premennú na nil pred blokom try a uvoľnite ju vo finally, ako to robí ukážka vyššie, aby vás zlyhanie v polovici nenechalo uvoľňovať nedefinovanú referenciu alebo úplne vynechať uvoľnenie.

Zmena poradia stránok bez ich prepisovania

Vyradenie (imposition) stavia nový dokument. Zmena poradia mení jeden dokument na mieste. Metóda MovePages zdvihne sadu stránok z ich aktuálnych pozícií a pustí ich na cieľové miesto, pričom posunie všetko ostatné okolo presunutého bloku tak, aby počet stránok zostal rovnaký:

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Indexy sú začínajúce od nuly (zero-based). PageIndices uvádza stránky, ktoré sa majú presunúť, v poradí, v akom majú skončiť, a DestPageIndex je index, na ktorom pristane prvá presunutá stránka po usadení presunu. Pretože PDFium premiestňuje stránky namiesto kopírovania a opätovnej kompresie ich obsahu, operácia je lacná a bezstratová: objekty stránok si zachovávajú svoje streamy, svoje zdroje a vernosť (fidelity). Toto je volanie za panelom stránok s presúvaním myšou (drag-to-reorder), kde používateľ potiahne náhľad do nového slotu a vy potvrdíte nové poradie jedným presunom. Vracia False, keď je index mimo rozsahu, takže validujte výsledok namiesto predpokladu, že reorganizácia prebehla.

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

Vytiahnutie podmnožiny podľa indexu

Tretia operácia kopíruje explicitnú sadu stránok z jedného dokumentu do druhého. ImportPagesByIndex vezme zdrojový dokument a pole indexov od nuly a vloží tieto stránky do cieľa na zvolenej pozícii:

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

Voláte ho na cieľovom dokumente a odovzdávate zdroj ako prvý argument. PageIndices určuje zdrojové stránky, ktoré sa majú vytiahnuť, v poradí, v akom ich chcete; InsertAt je slot od nuly v cieli, kam ide prvá importovaná stránka, takže 0 ich umiestni pred existujúcu prvú stránku a aktuálny počet stránok cieľa sa navýši. Prázdne pole importuje každú stránku, čo robí volanie úplnou kópiou, keď ju potrebujete. Vracia False, ak je akýkoľvek index mimo rozsahu v zdroji.

Tu je dôležitý kontrast s rozdelením (split). Rozdelenie zapisuje samostatné súbory, kedy jedna operácia produkuje mnoho výstupov na disku. ImportPagesByIndex robí opačný typ práce: zhromažďuje vybranú sadu stránok do jedného cieľového dokumentu v pamäti, ktorý potom uložíte raz. Keď je úlohou „daj mi stránky 3, 7 a 12 ako jedno krátke PDF“, toto je priama cesta, ktorá pod kapotou obaluje FPDF_ImportPagesByIndex.

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

Čisté prepojenie postupu

Celkový postup je rovnaký pri všetkých troch: otvoriť zdroj nastavením FileName a prepnutím Active na True, vykonať operáciu, uložiť pomocou SaveAs a uvoľniť to, čo vlastníte. Jednou vetvou, ktorá si vyžaduje opatrnosť, je to, ktoré volania alokujú nový dokument. MovePages mení dokument, ktorý už držíte, takže existuje jeden objekt na uvoľnenie. ImportPagesByIndex zapisuje do cieľa, ktorý ste sami vytvorili, takže uvoľníte zdroj aj otvorený cieľ. ImportNPagesToOne je výnimkou, pretože nový dokument je návratovou hodnotou metódy a nie niečím, čo ste vytvorili, a zabudnutie na to, že ide o samostatný handle vlastnený volajúcim, je spôsobom, ako dochádza k úniku aj dvojitému uvoľneniu. Inicializujte výsledok na nil, skontrolujte ho po volaní a uvoľnite ho na jedinej ceste.

Ak je prácou, ktorú skutočne máte, spájanie celých súborov a nie zmena usporiadania stránok, pozrite si zlúčenie viacerých súborov PDF do jedného dokumentu. Ak je to naopak, rozdelenie jedného dokumentu do niekoľkých súborov, pozrite si rozdelenie PDF dokumentov do viacerých súborov. Metódy vyradenia a zmeny poradia stránok popísané tu sa dodávajú ako súčasť PDFium Component pre Delphi a C++Builder spolu s API na načítanie, vykresľovanie a úpravu, ktoré sú popísané inde na tomto blogu.