Tekninen artikkeli

Uudelleenkäytettävät sivuleimat PDFiumin Form XObjecteilla

Vesileiman tai logon leimaaminen asiakirjan jokaiselle sivulle näyttää viiden minuutin työltä, kunnes avaat tuloksen tiedostokoon tarkastelijassa. Ilmeinen lähestymistapa on käydä sivut läpi ja rakentaa jokaiselle sivulle sama teksti- tai kuvaolio uudelleen. Se toimii visuaalisesti, mutta on kumuloituvalla tavalla tuhlaava. Jos diagonaalinen "DRAFT"-vesileima piirretään suoraan sat-sivuisen raportin päälle, sisältövirroissa istuu sata kopiota samasta polku- ja tekstidatasta, ja tallennettu tiedosto kuljettaa jokaisen niistä mukanaan

Form XObject on PDF:n tarjoama rakenne juuri tämän välttämiseksi. Se paketoi uudelleenkäytettävän sisällön, kokonaisen sivun tai pienen mallin, yhdeksi nimetuksi olioksi, joka voidaan maalata monta kertaa moniin sijainteihin. Sisältö elää tiedostossa kerran. Jokainen sivu, joka haluaa leiman, säilyttää lyhyen ohjeen joka sanoo "maalaa XObject N tähän tällä muunnoksella". Sadan sivun vesileima lisää tiedostoon näin yhden sisältöolion sadan sijasta, ja juuri siinä on ero asiakirjan välillä, joka kasvaa lineaarisesti sivumäärän mukana, ja sellaisen joka ei kasva. Vesileimat, logoleimat, sivunumeromallit ja sinetit ovat kaikki samaa ongelmamuotoa, ja Form XObject on oikea työkalu jokaiseen niistä

Miksi yksi tallennettu olio voittaa sata uudelleenpiirtoa

Säästö on rakenteellinen eikä kosmeettinen. PDF-sivu renderöidään suorittamalla sen sisältövirta, siis piirtokomentojen sarja. Kun leima piirretään uudelleen jokaiselle sivulle, leiman täydellinen komentojono liitetään jokaisen sivun virtaan ja tavut monistuvat yhtä monta kertaa kuin sivuja on. Form XObject siirtää nuo operaattorit yhteen virtaan, joka tallennetaan asiakirjaan kerran. Yksittäisen sivun säilyttämä viite on pieni: se puskee muunnosmatriisin, kutsuu XObjectia ja palauttaa tilan. Sivumäärä ei enää moninkertaista kuvituksen kustannusta

Tällä on suurin merkitys silloin, kun leima on raskas. Satoja polkusegmenttejä sisältävä vektorisinetti tai logobittikartta on kallis tallentaa. Kun se tallennetaan kerran ja viitataan siihen, raskas osa maksetaan vain kerran ja sivukohtainen yleiskustannus on muutama tavun pituinen kutsu. Visuaalinen lopputulos sivulla on identtinen suoran uudelleenpiirron kanssa, mikä on koko pointti. Lukija ei näe eroa; tiedostokoko kyllä näkee

Sivun kaappaaminen XObjectiksi

PDFium rakentaa uudelleenkäytettävän olion olemassa olevasta sivusta. Lähde voi olla jonkin avoinna olevan asiakirjan sivu, pieni yksisivuinen PDF joka sisältää vain vesileimakuvituksen, tai jonkin suuremman tiedoston tietty sivu. CreateXObjectFromPage kaappaa tuon lähdesivun sisällön uudelleenkäytettäväksi kahvaksi, joka kuuluu kohdeasiakirjalle, eli sille jota olet leimaamassa

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := 'Report.pdf';
    Dest.Active := True;
    Stamp.FileName := 'Watermark.pdf';   // one page of artwork
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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 heittää poikkeuksen, jos lähdeasiakirja ei ole Active, ja palauttaa nil-arvon poikkeuksen sijasta silloin kun PDFium ei pysty rakentamaan oliota, joten yllä oleva eksplisiittinen tarkistus ei ole valinnainen. Palautuva kahva on omistamasi TPdfXObject, ja siihen liittyvät kaksi elinkaarirajoitetta ovat juuri se osa tätä harjoitusta, johon ihmiset kompastuvat, joten niille on oma osionsa alempana

Leiman sijoittaminen sivulle

Kaapattu XObject ei tee itsessään mitään. Jotta se näkyy, lisäät siitä kopion asiakirjan nykyiselle sivulle, jonka valitsee 1-pohjainen PageNumber-ominaisuus, käyttäen InsertFormObjectFromXObject-metodia. Tuo kutsu palauttaa alla olevan sivuolion, eli FPDF_PAGEOBJECT-kahvan, ja juuri tällä palautuvalla kahvalla sijoitus asemoidaan. Ilman muunnosta leima päätyy origoon lähdesivun omassa koordinaatistossa, mikä on harvoin haluttu paikka

Koska InsertFormObjectFromXObject lisää yhden kopion kutsua kohden ja palauttaa joka kerta uuden sivuolion, sama XObject voidaan maalata yhdelle sivulle useita kertoja eri muunnoksilla, ja tallennettu sisältö lasketaan silti tiedostossa vain kerran. Kulmalogo ja haalea koko sivun vesileima voivat tulla samasta kaapatusta oliosta

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
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);
    RawM := M.Handle;
    if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
      raise Exception.Create('Cannot assign the stamp matrix');
  finally
    M.Free;
  end;
  Dest.UpdatePage;   // commit this page's edits to its content stream
  // if not Dest.SaveAs(...) then ... when every page is done.
end;

Kaksi ylläpitoyksityiskohtaa tekee tästä turvallista. Ensinnäkin, kun sivuolio on lisätty, se kuuluu sivulle eikä XObjectille. XObjectin vapauttaminen myöhemmin ei mitätöi jo tehtyjä sijoituksia. Juuri tämä mahdollistaa alla kuvatun create-place-free-järjestyksen. Toiseksi, lisääminen ja sijoittaminen muuttavat vain sivun olioluetteloa muistissa; UpdatePage serialisoi tämän luettelon takaisin sivun sisältövirtaan, joten sivu jota muokataan ilman tuota kutsua tallentuu aivan kuin leimaa ei olisi koskaan lisätty

Kahvan elinkaarisääntö, johon ihmiset kompastuvat

XObject-kahvaa säätelee kaksi rajoitetta, ja kummankin sivuuttaminen aiheuttaa virheen, joka näyttää irralliselta todellisesta syystään. Ensinnäkin lähdeasiakirjan on oltava aktiivinen sillä hetkellä kun kutsut CreateXObjectFromPage-metodia. Kaappaus lukee lähdesivun sisällön elävästä lähdeasiakirjasta, joten tuon asiakirjan ja sen sivun täytyy olla auki ja kelvollisia kahvan rakentamishetkellä. Toiseksi, ja tämä on se joka yllättää ihmiset, kahva täytyy vapauttaa ennen kuin lähdesivu suljetaan, ja käytännössä ennen kuin suljet tai vapautat lähdeasiakirjan, josta se tuli

Syy on se, että XObject on viite rakenteeseen, jonka lähdeasiakirja edelleen omistaa. Se ei ole irrallinen, omavarainen kopio jota voisi kantaa mukana sen jälkeen kun lähde on poissa. Jos lähde suljetaan ensin, kahva jää osoittamaan sisältöä, joka on jo purettu, jolloin sen myöhempi vapauttaminen tai mikä tahansa muu käyttö toimii muistilla, joka ei ole enää kelvollinen. Oire on klassinen roikkuvan kahvan oire: access violation sovelluksen sulkeutuessa, tai ajoittainen korruptio joka siirtyilee allokointijärjestyksen mukaan, ja pino osoittaa siivouskoodiin eikä riviin, joka todellisuudessa aiheutti ongelman. Korjaus on järjestys, ei puolustuskoodaus. Rakenna XObject, lisää se jokaiselle sivulle joka sitä tarvitsee, vapauta XObject ja vasta sitten sulje lähdeasiakirja. TPdfXObject-destruktori vapauttaa puolestasi alla olevan PDFium-kahvan, joten wrapperin vapauttaminen oikeaan aikaan on koko vastuusi

Matriisi ja sen kuuden luvun merkitys

Sijoitus on 2D-affiini muunnos, sama jota PDF käyttää kaikkialla sisällön asemointiin (ISO 32000-1, kohta 8.3.4). Se koostuu kuudesta luvusta, a, b, c, d, e, f, ja PDFium tarjoaa ne tietueena FS_MATRIX. Ne vievät pisteen olion omasta avaruudesta sivuavaruuteen

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

Nuo kuusi arvoa voi täyttää käsin, mutta niiden koostaminen käsin on se kohta, jossa rotaatio menee pieleen, koska kierto sotkee kaikki neljä arvoa a, b, c, d yhteen. TPdfMatrix-wrapperi yksiköstä FPdfMatrix koostaa yleiset operaatiot puolestasi ja kertolaskutetaan oikeassa järjestyksessä, joten Translate, Scale ja Rotate ketjuuntuvat siinä järjestyksessä jossa niitä kutsut. Diagonaalinen vesileima on kierto, jota seuraa siirto keskittämistä varten; kulmalogo on skaalaus, jota seuraa siirto. Kun matriisi on valmis, kopioi sen raakaarvo, eli Handle-ominaisuus tyypillä FS_MATRIX, paikalliseen muuttujaan ja anna se sitten FPDFPageObj_SetMatrix-funktiolle; tuonnissa matriisi on määritelty var-parametriksi, joten ominaisuutta ei voi antaa sille suoraan, ja sen tulos on 0 epäonnistumisen tapauksessa. Alemman tason FPDFPageObj_Transform, joka ottaa kuusi arvoa suoraan doubleina, on käytettävissä kun haluat mieluummin välittää luvut kuin rakentaa wrapperin

Jokaisen sivun leimaaminen oikeassa järjestyksessä

Kokonainen malli kokoaa palaset yhteen siinä järjestyksessä, jota elinkaarisääntö vaatii. Avaa molemmat asiakirjat, kaappaa leima kerran, käy kohdeasiakirjan sivut läpi asettamalla 1-pohjainen PageNumber vuorollaan ja lisää sekä sijoita kopio, varmista jokainen sivu kutsumalla UpdatePage, vapauta sitten XObject, tallenna lopuksi SaveAs-kutsulla ja anna lähdeasiakirjan sulkeutua viimeisenä

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
  I: Integer;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := ASource;
    Dest.Active := True;
    Stamp.FileName := AStamp;
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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. PageNumber is 1-based.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := 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
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // commit this page's edits
      end;
    finally
      XObject.Free;                          // 3. free BEFORE Stamp closes
    end;

    // 4. Write the result while Dest is still open.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // source closes last
    Dest.Free;
  end;
end;

try-lohkojen muoto tekee varsinaisen työn. Sisempi finally vapauttaa XObjectin ennen kuin ohjaus voi koskaan saavuttaa ulomman finally-lohkon, joka vapauttaa Stamp-olion, joten kahva vapautetaan aina sillä aikaa kun sen lähde on vielä elossa, vaikka poikkeus sinkoaisi ulos kesken silmukan. Kun tämä sisäkkäisyys on oikein, elinkaarisääntö huolehtii itsestään

Leimaus on yksi kulma laajemmassa työkalupakissa sivusisällön rakentamiseen ja muokkaamiseen. Jos leimasi on itse kuva eikä kaapattu sivu, artikkeli kuvien muuntamisesta PDF-asiakirjoiksi PDFiumilla kattaa sen, miten bittikartta saadaan ensin asiakirjaan. Ja kun näkyvän leiman rinnalla kuljetettava asia on tiedosto eikä muste sivulla, artikkeli PDF-liitteiden käsittelystä Delphissä näyttää sulautettujen tiedostojen puolen. Kaikki tämä toimitetaan yhdessä Delphiä ja C++Builderia varten tarkoitetun PDFium Component -tuotteen kanssa muiden tässä blogissa käsiteltyjen renderöinti-, muokkaus- ja asiakirja-APIen rinnalla