Technical Article

N-up kilövés és oldalátrendezés PDFium segítségével

Az összefűzés és a szétvágás az a két oldal-művelet, amelyhez mindenki először nyúl, és sok esetet lefednek. De nem fednek le mindent. Ezért létezik egy különálló feladatcsalád, amely az oldalak elrendezését módosítja, nem pedig egész fájlokat mozgat: helyezzen el négy diát egy lapra egy segédlethez, húzzon egy oldalt a dokumentum hátuljáról az elejére, vagy húzza ki a 3., 7. és 12. oldalt egy rövid kivonatba a többi érintése nélkül. A PDFium pontosan erre három metódust tesz közzé, és mindegyik eltérően viselkedik a már ismert összefűzéstől és szétvágástól. Ez a cikk végigmegy azon, hogy mit csinálnak, hol élnek a kimeneti pontok, és bemutat egy tulajdonjogi részletet, amely összeomlást okozott a gyakorlatban.

A három metódus a ImportNPagesToOne az N-up kilövéshez (imposition), a MovePages a helybeni átrendezéshez, és a ImportPagesByIndex a részhalmaz kinyeréséhez. Az összefűzés egymás mögé rakja a dokumentumokat, és az oldalszámot a bemenetek összegével hagyja egyenlőnek. A szétvágás több kimeneti fájlt ír ki egyetlen bemenetből. Az itt bemutatott három művelet a kettő között helyezkedik el: az egyik megváltoztatja, hány forrásoldal osztozik egy lapon, a másik megváltoztatja a sorrendet egyetlen dokumentumon belül, a harmadik pedig a kiválasztott oldalak maroknyi részét másolja át egy másik dokumentumba. Ha tudja, melyik mire való, megkíméli magát egy erőltetett összefűzési-és-törlési tánctól, amikor egyetlen hívás is elegendő lenne.

Mit csinál valójában az N-up kilövés

A kilövés (imposition) a nyomdai kifejezés arra, hogy több forrásoldalt rendeznek el egyetlen nagyobb lapon úgy, hogy a kinyomtatott és összehajtott eredmény a megfelelő sorrendben legyen olvasható. A mindennapi változat a 2-up segédlet, a 4-up füzet vagy a kontaktlap, amely egy tucat bélyegképet helyez el egy oldalon. A PDFium egyetlen hívással kezeli a geometriát:

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

NumX és NumY írja le a rácsot. A 2, 1 érték két forrásoldalt helyez el egymás mellé; a 2, 2 négyet csomagol egy negyedelt elrendezésbe; a 4, 3 tizenkét-up kontaktlapot épít fel. A PDFium sorrendben olvassa a forrásoldalakat, mindegyiket leméretezi, hogy beleférjen a cellájába, és balról jobbra, fentről lefelé tölti fel a rácsot, új kimeneti lapot kezdve, amikor az aktuális rács megtelik. A forrásoldalak nem módosulnak. Amit visszakap, az egy új dokumentum, amelynek oldalai kompozitok.

A kimenet mérete pontban (points) van megadva, nem pixelben

A OutputWidth és OutputHeight PDF felhasználói egységek, egy PDF felhasználói egység pedig egy pont, ami egy inch 1/72 része. Az egység a kimeneti lap fizikai méretét adja meg, és semmi köze sincs a képernyőpixelekhez vagy a renderelési DPI-hez. Ez a leggyakoribb hely, ahol a kilövést elrontják, mert a bitképekhez szokott fejlesztő pixelek számát adja meg, és végül egy postai bélyeg vagy egy óriásplakát méretű lapot kap.

A leginkább megjegyzendő számok a leggyakrabban használt két oldalméret. A US Letter 612-szer 792 pont, mert 8,5 inch szorozva 72-vel az 612, és 11 inch szorozva 72-vel az 792. Az A4 nagyjából 595-ször 842 pont, a 210-szer 297 milliméteres méretéből adódóan. A kötés saját fejléce világosan kimondja a szabályt, hogy egy egység egy inch 1/72 része, és a kötés tartalmaz egy 72-vel egyenlő PointsPerInch konstanst, ha a kódban inkább inchekből számolná ki a méretet ahelyett, hogy literált írna be.

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;

A visszakapott kezelőt Önnek kell felszabadítania

Olvassa el újra a szignatúrát. Az ImportNPagesToOne egy TPdf-et ad vissza, nem pedig logikai értéket. Ez a visszatérési érték egy vadonatúj dokumentum-kezelő (handle), amelyet a forrástól függetlenül foglaltak le, és a hívó birtokolja. Az a forrás TPdf, amelyen a metódust meghívta, érintetlen, és továbbra is a saját kezelőjét birtokolja; a kompozit egy második, független objektum. Ha hagyja, hogy a visszakapott TPdf hatókörön kívülre kerüljön anélkül, hogy felszabadítaná, egy teljes PDFium dokumentumot szivárogtat el.

A veszélyesebb hiba a másik irányba fut. A háttérben a metódus egy friss FPDF_DOCUMENT-et kér a PDFium-tól az FPDF_ImportNPagesToOne segítségével, majd ezt a nyers kezelőt becsomagolja a visszakapott TPdf-be, így a csomagoló élettartama szabályozza a kezelőét. Ettől a ponttól kezdve pontosan egyetlen tulajdonosa van a kezelőnek, és pontosan egyetlen helyen szabad bezárni: amikor felszabadítja (Free) a visszakapott objektumot. Egy óvatlan hibaútvonal, amely felszabadítja a csomagolót, és meghívja a FPDF_CloseDocument-et is az általa elkapott nyers kezelőn, ugyanazt a PDFium dokumentumot kétszer zárja be. Ez egy többszörös felszabadítás (double-free), és ez volt az a konkrét hiba, ami egyszer megharapott egy hívót itt. Az ezt megakadályozó szabály rövid. Csak egyetlen útvonalon zárja be a dokumentumot, mégpedig a metódus által átadott TPdf felszabadításával, és soha ne nyúljon túl a csomagolón, hogy bezárja a már általa adoptált kezelőt.

Ebből két következtetés adódik. Elsőként, a metódus nil-t ad vissza, ha a PDFium elutasítja az argumentumokat, például ha valamelyik rácstengelyen nulla szerepel, vagy ha foglalási hiba történik, így a nil ellenőrzés kötelező, mielőtt hozzányúlna az eredményhez. Másodszor, inicializálja a kimeneti változót nil-re a try előtt, és szabadítsa fel a finally blokkban, ahogy a fenti minta teszi, hogy a menet közbeni hiba ne hagyja Önt egy nem definiált hivatkozás felszabadításában vagy a felszabadítás teljes kihagyásában.

Oldalak átrendezése újraírás nélkül

Az átrendezés egyetlen dokumentumot módosít helyben. A MovePages kiemel egy oldalcsoportot a jelenlegi helyéről, és leejti őket egy célállomáson, a mozgatott blokk körül minden mást eltolva, így az oldalszám változatlan marad:

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

Az indexek nulla alapúak. A PageIndices felsorolja a mozgatni kívánt oldalakat a kívánt sorrendben, és a DestPageIndex az az index, ahová az első mozgatott oldal megérkezik a mozgás elrendeződése után. Mivel a PDFium áthelyezi az oldalakat ahelyett, hogy lemásolná és újratömörítené a tartalmukat, a művelet olcsó és veszteségmentes: az oldalobjektumok megtartják az adatfolyamukat, az erőforrásaikat és a hűségüket. Ez a hívás áll a húzd-és-rendezd oldalpanel mögött, ahol a felhasználó áthúz egy bélyegképet egy új helyre, Ön pedig egyetlen mozdulattal véglegesíti az új sorrendet. False értéket ad vissza, ha egy index tartományon kívül esik, ezért ellenőrizze az eredményt, ahelyett, hogy feltételezné az átrendezés sikerét.

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;

Részhalmaz kinyerése index alapján

A harmadik művelet az oldalak egy meghatározott csoportját másolja át egyik dokumentumból a másikba. A ImportPagesByIndex fogadja a forrásdokumentumot és a nulla alapú index-tömböt, majd beilleszti ezeket az oldalakat a célba a kiválasztott helyen:

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

A céldokumentumon hívja meg, és a forrást adja át első argumentumként. A PageIndices megnevezi a lehúzandó forrásoldalakat a kívánt sorrendben; az InsertAt a nulla alapú hely a célban, ahová az első importált oldal kerül, így a 0 a meglévő első oldal elé helyezi őket, a cél aktuális oldalszáma pedig bővül. Egy üres tömb minden oldalt importál, ami teljes másolássá teszi a hívást, ha arra van szüksége. False értéket ad vissza, ha a forrásban bármelyik index tartományon kívül esik.

Itt számít a szétvágással való kontraszt. A szétvágás különálló fájlokat ír ki, egyetlen művelet sok kimenetet eredményez a lemezen. A ImportPagesByIndex az ellenkező irányú munkát végzi: a kiválasztott oldalak egy csoportját gyűjti össze egyetlen céldokumentumba a memóriában, amelyet aztán egyszer ment el. Amikor a feladat az, hogy "add meg a 3., 7. és 12. oldalt egyetlen rövid PDF-ként", ez a közvetlen út, és a háttérben az FPDF_ImportPagesByIndex-et csomagolja be.

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;

Tiszta összeszerelés

A teljes folyamat formája mindháromnál megegyezik: nyissa meg a forrást a FileName beállításával és az Active tulajdonság True-ra állításával, végezze el a műveletet, mentse el a SaveAs segítségével, és szabadítsa fel azt, amit birtokol. Az egyetlen ág, amely figyelmet igényel, az az, hogy mely hívások foglalnak le új dokumentumot. A MovePages módosítja a már kézben tartott dokumentumot, így egyetlen objektumot kell felszabadítani. A ImportPagesByIndex egy Ön által létrehozott célba ír, így felszabadítja a forrást és a megnyitott célt is. A ImportNPagesToOne a kivétel, mert az új dokumentum a metódus visszatérési értéke ahelyett, hogy Ön hozta volna létre, és ha elfelejti, hogy ez egy különálló, a hívó által birtokolt kezelő, az szivárgáshoz és többszörös felszabadításhoz vezet. Inicializálja az eredményt nil-re, ellenőrizze a hívás után, és szabadítsa fel egyetlen úton.

If the work you actually have is combining whole files rather than rearranging pages, see merging multiple PDF files into one document. If it is the reverse, breaking one document into several files, see splitting PDF documents into multiple files. The imposition and reordering methods described here ship as part of the PDFium Component for Delphi and C++Builder, alongside the loading, rendering, and editing APIs covered elsewhere on this blog.