Technical Article

Uudelleenkäytettävät sivuleimat Form XObject -objektien avulla PDFiumilla

Vesileiman tai logon leimaaminen dokumentin jokaiselle sivulle näyttää viiden minuutin työltä, kunnes avaat tuloksen tiedostokoon tarkastimessa. Ilmeisin lähestymistapa on kävellä sivut läpi ja rakentaa jokaiselle sivulle samat teksti- tai kuvaobjektit uudelleen. Se toimii visuaalisesti, ja se on tuhlausta tavalla, joka kerrannaisena kasvaa. Sadan sivun raporttiin suoraan piirretty viisto "LUONNOS"-vesileima tarkoittaa sataa kopiota samasta polku- ja tekstidatasta sisältövirroissa, ja tallennettu tiedosto kantaa niistä jokaisen.

Form XObject on rakenne, jonka PDF tarjoaa juuri tämän välttämiseksi. Se käärii uudelleenkäytettävän sisällön, koko sivun tai pienen mallipohjan, yhdeksi nimetyksi objektiksi, joka voidaan maalata monta kertaa useissa eri kohdissa. Sisältö elää tiedostossa kerran. Jokainen sivu, joka haluaa leiman, sisältää lyhyen ohjeen, joka sanoo: "maalaa XObject N tässä, tällä muunnoksella." Sadan sivun vesileima lisää tiedostoon silloin yhden sisältöobjektin sadan sijaan, ja se on ero sivumäärän mukaan lineaarisesti kasvavan dokumentin ja sellaisen välillä, joka ei kasva. Vesileimat, logoleimat, sivunumeromallit ja sinetit ovat kaikki saman muotoisia ongelmia, ja Form XObject on oikea työkalu niistä jokaiseen.

Miksi yksi tallennettu objekti voittaa sata uudelleenpiirtoa

Säästö on rakenteellista, ei kosmeettista. PDF-sivu renderöityy suorittamalla sen sisältövirran, joka on piirustusoperaattoreiden sarja. Kun piirät leiman uudelleen sivua kohden, lisäät leiman koko operaattorisarjan jokaisen sivun virtaan, ja tavut monistuvat niin monta kertaa kuin sinulla on sivuja. Form XObject siirtää nämä operaattorit yhteen virtaan, joka tallennetaan kerran dokumenttiin. Viite, jonka yksittäinen sivu säilyttää, on pieni: se työntää muunnosmatriisin, kutsuu XObjectia ja palauttaa tilan. Sivumäärä ei enää moninkertaista taideteoksen kustannuksia.

Tällä on eniten merkitystä silloin, kun leima on raskas. Vektorisinetit, joissa on satoja polkusegmenttejä, tai logon bittikartta, ovat kalliita tallentaa. Kerran tallennettuna ja viitattuna raskas osa maksetaan yhden kerran ja sivukohtainen ylikuormitus on muutaman tavun kutsu. Visuaalinen tulos sivulla on identtinen suoran uudelleenpiirron kanssa, mikä on koko homman ydin. Lukija ei huomaa eroa; tiedostokoko huomaa hyvinkin.

Sivun kaappaaminen XObject-objektiksi

PDFium rakentaa uudelleenkäytettävän objektin olemassa olevasta sivusta. Lähde on sivu jossakin avaamassasi dokumentissa, pieni yksisivuinen PDF, joka ei sisällä muuta kuin vesileimasi, tai tietty sivu suuremmasta tiedostosta. CreateXObjectFromPage kaappaa kyseisen lähdesivun sisällön uudelleenkäytettäväksi kahvaksi, jonka omistaa kohdedokumentti eli se, jota olet leimaamassa.

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create;
  Stamp := TPdf.Create;
  try
    Dest.LoadFromFile('Report.pdf');
    Stamp.LoadFromFile('Watermark.pdf');   // one page of artwork

    // Capture page 0 of the stamp document into a reusable handle that
    // is owned by Dest. Source must be active; the index is zero-based.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not build the stamp XObject');
    // ... place it, then free it before closing Stamp (see below) ...

Allekirjoitus on CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metodi palauttaa nil-arvon epäonnistuessa sen sijaan, että se nostaisi poikkeuksen, joten eksplitiittinen tarkistus ylhäällä ei ole valinnainen. Takaisin tuleva kahva on TPdfXObject, jonka omistat, ja kaksi siihen liittyvää elinkaarirajoitusta ovat se osa tätä harjoitusta, joka yllättää ihmiset, joten ne saavat oman osionsa alla.

Leiman asettaminen sivulle

Kaapattu XObject ei tee mitään itsestään. Jotta se saadaan näkyviin, asetat sen kopion dokumentin nykyiselle sivulle InsertFormObjectFromXObject-kutsulla. Tämä kutsu palauttaa taustalla olevan sivuobjektin, FPDF_PAGEOBJECT, ja palautettu kahva on se, jolla sijoittelu asemoidaan. Ilman muunnosta leima laskeutuu origoon lähdesivun omissa koordinaateissa, mikä on harvoin se, mitä haluat.

Koska InsertFormObjectFromXObject asettaa yhden kopion kutsua kohden ja antaa takaisin tuoreen sivuobjektin kahvan joka kerta, voit maalata saman XObject-objektin useita kertoja yhdelle sivulle eri muunnoksilla, ja tallennettu sisältö lasketaan silti kerran tiedostossa. Kulmassa oleva logo ja haalea kokosivun vesileima voivat tulla samasta kaapatusta objektista.

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
begin
  // The current page of Dest receives one copy of the XObject.
  PageObj := Dest.InsertFormObjectFromXObject(XObject);
  if PageObj = nil then
    raise Exception.Create('Insert failed on this page');

  // Position it: move 200 units right, 500 up, at 70% scale.
  M := TPdfMatrix.Create;
  try
    M.Scale(0.7, 0.7);
    M.Translate(200, 500);
    FPDFPageObj_SetMatrix(PageObj, M.Handle);
  finally
    M.Free;
  end;
  // Dest.SaveLoadedDocument(...) when every page is done.
end;

Yksi omistussuhde tekee siivouksesta turvallisen. Kun sivuobjekti on asetettu, se kuuluu sivulle, ei XObjectille. XObjectin vapauttaminen myöhemmin ei mitätöi jo tekemiäsi sijoitteluja. Se on se, mikä mahdollistaa alla kuvatun luo-sijoita-vapauta-järjestyksen toiminnan.

Elinkaarisääntö, joka puree ihmisiä

Kaksi rajoitusta hallitsee XObject-kahvaa, ja kummankin huomiotta jättäminen tuottaa virheen, joka näyttää olevan riippumaton syystään. Ensinnäkin lähdedokumentin on oltava aktiivinen hetkellä, jolloin kutsut CreateXObjectFromPage-metodia. Kaappaus lukee lähdesivun sisällön elävästä lähdedokumentista, joten kyseisen dokumentin ja sen sivun on oltava auki ja kelvollisia, kun kahva rakennetaan. Toiseksi, ja tämä on se, joka yllättää ihmiset, kahva on vapautettava ennen lähdesivun sulkemista ja käytännössä ennen kuin suljet tai vapautat lähdedokumentin, josta se tuli.

Syy on se, että XObject on viite rakenteeseen, jonka lähdedokumentti edelleen omistaa. Se ei ole irrallinen, itsenäinen kopio, jota voit kantaa mukanasi sen jälkeen, kun lähde on poissa. Sulje lähde ensin, ja kahva jää osoittamaan sisältöön, joka on purettu, joten sen vapauttaminen myöhemmin tai mikä tahansa muu käyttö kohdistuu muistiin, joka ei ole enää kelvollista. Oireena on tyypillinen roikkuvan kahvan oire: muistivirhe sammutettaessa tai ajoittainen korruptio, joka siirtyy ympäriinsä riippuen varausjärjestyksestä, pinon osoittaessa siivouskoodiin eikä koodiriviin, joka todellisuudessa aiheutti ongelman. Korjaus on järjestys, ei puolustava koodaus. Rakenna XObject, aseta se jokaiselle sivulle, joka sitä tarvitsee, vapauta XObject ja vasta sen jälkeen sulje lähdedokumentti. TPdfXObject-destruktori vapauttaa taustalla olevan PDFium-kahvan puolestasi, joten wrapperin vapauttaminen oikeaan aikaan on koko vastuusi.

Matriisi ja mitä sen kuusi lukua tarkoittavat

Sijoittelu on 2D affiini muunnos, sama jota PDF käyttää kaikkialla sisällön sijoittamiseen. Se on kuusi lukua, jotka kirjoitetaan muodossa a, b, c, d, e, f, ja PDFium paljastaa ne FS_MATRIX-tietueena. Ne kuvaavat pisteen objektin omasta tilasta sivutilaan:

// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)

Voit täyttää nämä kuusi arvoa käsin, mutta niiden yhdistäminen käsin on kohta, jossa pyöritys menee helposti väärin, koska pyöritys sekoittaa kaikki neljä arvoa a, b, c, d keskenään. TPdfMatrix-wrapperi yhdistää yleiset operaatiot puolestasi ja kertoo ne peräkkäin, joten Translate, Scale ja Rotate ketjuttuvat siinä järjestyksessä kuin kutsut niitä. Viisto vesileima on pyöritys, jota seuraa siirto sen keskittämiseksi uudelleen; kulmassa oleva logo on mittakaavan muutos, jota seuraa siirto. Kun matriisi on valmis, anna sen raaka-arvo kutsulle FPDFPageObj_SetMatrix(PageObj, M.Handle), jossa M.Handle on taustalla oleva FS_MATRIX. Alemman tason FPDFPageObj_Transform, joka ottaa kuusi arvoa suoraan double-tyyppinä, on käytettävissä, kun haluat mieluummin välittää lukuja kuin rakentaa wrapperin.

Jokaisen sivun leimaaminen oikeassa järjestyksessä

Koko kuvio yhdistää osat elinkaarisäännön vaatimaan järjestykseen. Avaa molemmat dokumentit, kaappaa leima kerran, käy läpi kohdesivut valitsemalla kukin vuorollaan ja asettamalla sekä asemoimalla kopio, vapauta sitten XObject, tallenna ja anna lähdedokumentin sulkeutua viimeisenä.

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  i: Integer;
begin
  Dest := TPdf.Create;
  Stamp := TPdf.Create;
  try
    Dest.LoadFromFile(ASource);
    Stamp.LoadFromFile(AStamp);

    // 1. Capture the artwork once. Stamp is active here.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not capture the stamp page');
    try
      // 2. Place a copy on every page of Dest.
      for i := 0 to Dest.PageCount - 1 do
      begin
        Dest.CurrentPageIndex := i;          // make page i current
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);                      // diagonal watermark
          M.Translate(150, 100);             // nudge into position
          FPDFPageObj_SetMatrix(PageObj, M.Handle);
        finally
          M.Free;
        end;
      end;
    finally
      XObject.Free;                          // 3. free BEFORE Stamp closes
    end;

    // 4. Write the result while Dest is still open.
    Dest.SaveLoadedDocument(AOutput);
  finally
    Stamp.Free;                              // source closes last
    Dest.Free;
  end;
end;

try-lohkojen muoto tekee todellisen työn. Sisempi finally vapauttaa XObjectin ennen kuin suoritus voi koskaan saavuttaa ulompaa finally-lohkoa, joka vapauttaa Stamp-objektin, joten kahva vapautetaan aina, kun sen lähde on vielä elossa, vaikka poikkeus laukeaisi kesken silmukan. Tee tämä sisäkkäisyys oikein, ja elinkaarisääntö huolehtii itsestään. (Käytä mitä tahansa koontiversiosi paljastamaa nykyisen sivun valitsinta; silmukan runko on sama kummallakin tavalla.)

Leimaaminen on yksi osa laajempaa työkalupakkia sivun sisällön rakentamiseen ja muokkaamiseen. Jo leimasi on itsessään kuva kaapatun sivun sijaan, kuvien muuntaminen PDF-dokumenteiksi PDFiumilla kattaa kyseisen bittikartan saamisen dokumenttiin ensin. Ja kun asia, jota haluat kuljettaa näkyvän leiman rinnalla, on tiedosto eikä mustetta sivulla, PDF-liitteiden parissa työskentely Delphissä näyttää upotetun tiedoston puolen. Kaikki tämä toimitetaan Delphin ja C++Builderin PDFium Componentin mukana renderöinti-, muokkaus- ja dokumentti-API:en rinnalla, joita käsitellään muualla tässä blogissa.