Technical Article

Impozice N stránek na list a změna pořadí stránek v PDFium

Sloučení a rozdělení jsou dvě operace se stránkami, po kterých každý sáhne jako první, a pokrývají širokou škálu potřeb. Nepokrývají však vše. Existuje samostatná skupina úkolů, které stránky přeskupují, místo aby přesouvaly celé soubory: umístění čtyř snímků prezentace na jeden list jako podklad, přetažení stránky z konce dokumentu na začátek nebo vytažení stránek 3, 7 a 12 do krátkého výňatku bez ovlivnění zbytku. PDFium vystavuje pro tyto účely přesně tři metody a každá se chová jinak než sloučení a rozdělení, která již znáte. Tento článek popisuje, co dělají, kde se nacházejí výstupní body a jeden detail vlastnictví, který v praxi způsobil pády programů.

Těmito třemi metodami jsou ImportNPagesToOne pro impozici N stránek na list, MovePages pro změnu pořadí na místě a ImportPagesByIndex pro vytažení podmnožiny stránek. Sloučení řadí dokumenty za sebe a výsledný počet stránek se rovná součtu vstupů. Rozdělení zapisuje několik výstupních souborů z jednoho vstupu. Zde popsané tři operace stojí někde mezi: jedna z nich mění počet zdrojových stránek sdílejících list, druhá mění pořadí v rámci jednoho dokumentu a třetí kopíruje vybranou hrstku stránek do jiného dokumentu. Znalost toho, která metoda je která, vás ušetří složitého postupu slučování a mazání tam, kde by stačilo jedno volání.

Co impozice N stránek na list skutečně dělá

Impozice (vyřazování stránek) je předtiskový termín pro uspořádání několika zdrojových stránek na jeden větší list tak, aby vytištěný a složený výsledek měl správné pořadí stránek. Běžnou verzí jsou prezentace se 2 stránkami na list, 4stránkový sešitový podpis nebo kontaktní list, který na stránku vměstná tucet náhledů. PDFium řeší geometrii pomocí jednoho volání:

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

NumX a NumY popisují mřížku. Hodnota 2, 1 umístí dvě zdrojové stránky vedle sebe; 2, 2 seskupí čtyři stránky do rozvržení kvadrantů; 4, 3 vytvoří dvanáctistránkový kontaktní list. PDFium čte zdrojové stránky v pořadí, zmenší každou z nich tak, aby se vešla do své buňky, a plní mřížku zleva doprava a shora dolů, přičemž začne nový výstupní list, jakmile je aktuální mřížka plná. Zdrojové stránky se nemění. To, co získáte zpět, je nový dokument, jehož stránky jsou složené z více původních.

Velikost výstupu je v bodech, nikoli v pixelech

Parametry OutputWidth a OutputHeight jsou uživatelské jednotky PDF. Uživatelská jednotka PDF je jeden bod, což je jedna dvaasedmdesátina palce. Tato jednotka definuje fyzickou velikost výstupního listu a nemá nic společného s pixely na obrazovce nebo DPI při vykreslování. To je nejčastější místo, kde se při impozici dělá chyba, protože vývojář zvyklý na bitmapy zvolí počet pixelů a skončí s listem velikosti poštovní známky nebo billboardu.

Čísla, která stojí za to si zapamatovat, jsou dvě nejčastěji používané velikosti stránek. Formát US Letter má 612 × 792 bodů, protože 8,5 palce krát 72 je 612 a 11 palců krát 72 je 792. Formát A4 má přibližně 595 × 842 bodů, což odpovídá jeho rozměrům 210 × 297 milimetrů. Samotná hlavička vazby jasně stanovuje pravidlo, že jedna jednotka je jedna dvaasedmdesátina palce, a jednotka obsahuje konstantu PointsPerInch rovnou 72, pokud byste raději velikost z palců počítali v kódu než psali doslovnou hodnotu.

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ácený handle musíte uvolnit vy sami

Přečtěte si signaturu znovu. Metoda ImportNPagesToOne vrací TPdf, nikoli Boolean. Tato návratová hodnota je zcela nový handle dokumentu alokovaný odděleně od zdroje a volající jej vlastní. Zdrojový objekt TPdf, nad kterým jste metodu volali, zůstává nedotčen a stále vlastní svůj vlastní handle. Složený dokument je druhým, nezávislým objektem. Pokud necháte vrácený TPdf zaniknout bez uvolnění, dojde k úniku celého dokumentu PDFium z paměti.

Nebezpečnější chyba se děje opačným směrem. Vnitřně si metoda vyžádá od PDFium nový FPDF_DOCUMENT prostřednictvím FPDF_ImportNPagesToOne a poté tento surový handle zabalí do vráceného objektu TPdf, takže životnost obalu řídí životnost handlu. Od tohoto okamžiku existuje právě jeden vlastník handlu a právě jedno místo, kde by měl být uzavřen: při volání Free nad vráceným objektem. Neopatrná chybová cesta, která uvolní obal a zároveň zavolá FPDF_CloseDocument na zachycený surový handle, uzavře stejný dokument PDFium dvakrát. Jedná se o dvojité uvolnění paměti a to je konkrétní chyba, která zde jednou postihla volajícího. Pravidlo, které tomu předchází, je jednoduché. Uzavřete dokument pouze na jedné cestě, a to uvolněním objektu TPdf, který vám metoda předala, a nikdy se nesnažte přistupovat za obal, abyste uzavřeli handle, který již přijal za svůj. Z toho vyplývají dva důsledky. Za prvé, metoda vrací nil, pokud PDFium odmítne argumenty, například nulu na některé z os mřížky nebo při selhání alokace, takže kontrola na nil je na místě dříve, než s výsledkem začnete pracovat. Za druhé, před blokem try inicializujte výstupní proměnnou na nil a v bloku finally ji uvolněte, jak ukazuje ukázka výše, aby vás selhání v polovině postupu nenechalo uvolňovat nedefinovanou referenci nebo uvolnění zcela vynechat.

Změna pořadí stránek bez jejich přepisování

Impozice vytváří nový dokument. Změna pořadí upravuje stávající dokument na místě. Metoda MovePages vyjme sadu stránek z jejich aktuálních pozic a vloží je na cílové místo, přičemž posune vše ostatní kolem přesunutého bloku tak, aby počet stránek zůstal stejný:

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

Indexy jsou číslovány od nuly. PageIndices uvádí stránky k přesunu v pořadí, v jakém mají skončit, a DestPageIndex je index, na kterém první přesunutá stránka skončí po dokončení přesunu. Vzhledem k tomu, že PDFium stránky přemisťuje a nekopíruje ani znovu nekomprimuje jejich obsah, je operace rychlá a bezztrátová: objekty stránek si zachovávají své streamy, své prostředky a svou kvalitu. Toto volání stojí za panelem stránek s možností přetahování, kde uživatel přesune miniaturu na novou pozici a vy potvrdíte nové pořadí jedním přesunem. Pokud je index mimo rozsah, vrátí hodnotu False. Výsledek proto ověřujte, místo abyste předpokládali, že přesun proběhl v pořádku.

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;

Vytažení podmnožiny stránek podle indexu

Třetí operace kopíruje explicitní sadu stránek z jednoho dokumentu do druhého. Metoda ImportPagesByIndex přijímá zdrojový dokument a pole indexů číslovaných od nuly a vloží tyto stránky do cílového dokumentu na zvolenou pozici:

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

Voláte ji nad cílovým dokumentem a jako první argument předáváte zdroj. PageIndices určuje zdrojové stránky, které se mají vytáhnout, v požadovaném pořadí; InsertAt je pozice v cílovém dokumentu (číslovaná od nuly), kam se vloží první importovaná stránka, takže hodnota 0 je zařadí před stávající první stránku, zatímco předání aktuálního počtu stránek cílového dokumentu je připojí na konec. Prázdné pole importuje všechny stránky, což z volání dělá úplnou kopii, pokud ji potřebujete. Vrátí hodnotu False, pokud je některý index ve zdroji mimo rozsah. Zde se projevuje rozdíl oproti rozdělení. Rozdělení zapisuje samostatné soubory, kdy jedna operace vytváří mnoho výstupů na disku. Metoda ImportPagesByIndex provádí opačnou práci: shromáždí vybranou sadu stránek do jediného cílového dokumentu v paměti, který pak jednou uložíte. Pokud zní úkol „dej mi stránky 3, 7 a 12 jako jedno krátké PDF“, toto je přímá cesta, která na pozadí volá 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é sestavení celého postupu

Celkový postup je u všech tří operací stejný: otevřít zdroj nastavením FileName a přepnutím Active na True, provést operaci, uložit pomocí SaveAs a uvolnit to, co vlastníte. Jedinou větví, která vyžaduje péči, je ta, která alokuje nový dokument. Metoda MovePages mění dokument, který již držíte, takže je třeba uvolnit pouze jeden objekt. Metoda ImportPagesByIndex zapisuje do cíle, který jste sami vytvořili, takže uvolníte zdroj i cíl, který jste otevřeli. Metoda ImportNPagesToOne je výjimkou, protože nový dokument je návratovou hodnotou metody a nikoli něčím, co jste sami vytvořili. Zapomenutí na to, že jde o samostatný handle vlastněný volajícím, je způsobem, jakým dochází k úniku paměti i dvojitému uvolnění. Inicializujte výsledek na nil, po volání jej zkontrolujte a uvolněte jej pouze na jediné cestě. Pokud je vaším úkolem spojování celých souborů a nikoli přerovnávání stránek, podívejte se na článek slučování více souborů PDF do jednoho dokumentu. Pokud jde o opačný úkol, tedy rozdělení jednoho dokumentu do několika souborů, podívejte se na článek rozdělení dokumentů PDF do více souborů. Metody impozice a změny pořadí stránek popsané v tomto článku jsou dodávány jako součást PDFium Component pro Delphi a C++Builder společně s rozhraními API pro načítání, vykreslování a úpravy popsanými jinde na tomto blogu.