Techninis straipsnis

Daugkartinio naudojimo puslapių spaudai per „Form XObjects“ naudojant PDFium

Vandens ženklo arba logotipo uždėjimas ant kiekvieno dokumento puslapio atrodo kaip penkių minučių darbas, kol neatidarote rezultato failo dydžio tikrinimo įrankyje. Akivaizdus būdas yra pereiti puslapius ir kiekviename iš jų vėl sukurti tuos pačius teksto ar vaizdo objektus. Vizualiai tai veikia, tačiau tai sukelia vis didėjantį resursų švaistymą. Įstrižas „JUODRAŠTIS“ (DRAFT) vandens ženklas, nupieštas tiesiai ant šimto puslapių ataskaitos, yra šimtas to paties kelio ir teksto duomenų kopijų, esančių turinio srautuose (content streams), ir išsaugotame faile yra kiekviena iš jų

„Form XObject“ yra PDF struktūra, skirta būtent to išvengti. Ji apgaubia daugkartinio naudojimo turinio dalį – visą puslapį ar mažą šabloną – į vieną pavadintą objektą, kurį galima atvaizduoti daug kartų skirtingose pozicijose. Turinys faile egzistuoja vieną kartą. Kiekvienas puslapis, kuriame norima uždėti spaudą, turi trumpą instrukciją, sakančią: „nupieškite XObject N čia, su šia transformacija“. Taip šimto puslapių vandens ženklas faile prideda vieną turinio objektą vietoj šimto, ir tai yra skirtumas tarp dokumento, kuris auga tiesiogiai proporcingai jo puslapių skaičiui, ir to, kuris neauga. Vandens ženklai, logotipų spaudai, puslapių numerių šablonai ir antspaudai yra tos pačios problemos forma, o „Form XObject“ yra tinkamas įrankis kiekvienam iš jų

Kodėl vienas išsaugotas objektas yra geriau nei šimtas perpiešimų

Sutaupymas yra struktūrinis, o ne kosmetinis. PDF puslapis atvaizduojamas vykdant jo turinio srautą (content stream) – piešimo operatorių seką. Kai kiekviename puslapyje perpiešiate spaudą, to spaudo pilną operatorių seką pridedate prie kiekvieno puslapio srauto, o baitai dubliuojami tiek kartų, kiek yra puslapių. „Form XObject“ perkelia tuos operatorius į vieną srautą, išsaugotą dokumente vieną kartą. Nuoroda, kurią išlaiko atskiras puslapis, yra maža: ji pritaiko transformacijos matricą, iškviečia „XObject“ ir atkuria būseną. Puslapių skaičius nebedaugina grafikos dydžio kaštų

Tai labiausiai svarbu, kai spaudas yra „sunkus“. Vektorinį antspaudą su šimtais kelio segmentų arba logotipo taškinę grafiką (bitmap) saugoti yra brangu. Išsaugojus vieną kartą ir pateikus nuorodą, sunkioji dalis apmokama vieną kartą, o papildomos išlaidos puslapiui tėra keli iškvietimo baitai. Vizualus rezultatas puslapyje yra identiškas tiesioginiam perpiešimui – tai ir yra esmė. Skaitytojas nepastebės skirtumo; failo dydis tai tikrai pastebės

Puslapio pavertimas į „XObject“

PDFium sukuria daugkartinio naudojimo objektą iš esamo puslapio. Šaltinis yra puslapis tam tikrame jūsų atidarytame dokumente, pavyzdžiui, mažame vieno puslapio PDF faile, kuriame yra tik jūsų vandens ženklo grafika, arba konkrečiame didesnio failo puslapyje. CreateXObjectFromPage perima šio šaltinio puslapio turinį į daugkartinio naudojimo rodyklę (handle), kuri priklauso tiksliniam dokumentui – tam, kurį jūs spauduojate

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

Signatūra yra CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metodas iškelia klaidą (raises exception), jei šaltinio dokumentas nėra Active, ir grąžina nil užuot iškėlęs klaidą, kai PDFium negali sukurti objekto, todėl aukščiau pateiktas aiškus tikrinimas yra būtinas. Grąžinama rodyklė yra jums priklausantis TPdfXObject, o du su juo susiję gyvavimo trukmės apribojimai yra ta šios procedūros dalis, dėl kurios žmonės dažnai klysta, todėl jiems skirta atskira skiltis žemiau

Spaudo dėjimas puslapyje

Pats savaime perimtas „XObject“ nedaro nieko. Kad jis atsirastų, turite įterpti jo kopiją į dabartinį dokumento puslapį (pasirinktą per nuo 1 pradedamą PageNumber savybę), naudojant InsertFormObjectFromXObject. Šis iškvietimas grąžina pagrindinį puslapio objektą, FPDF_PAGEOBJECT, o grąžinta rodyklė naudojama spaudui pozicionuoti. Be transformacijos spaudas atsiduria koordinačių pradžios taške pradinio puslapio koordinatėse, o to paprastai nenorite

Kadangi InsertFormObjectFromXObject įterpia po vieną kopiją kiekvienam iškvietimui ir kiekvieną kartą grąžina naują puslapio objektą, galite nupiešti tą patį „XObject“ kelis kartus tame pačiame puslapyje su skirtingomis transformacijomis, o išsaugotas turinys faile vis tiek bus skaičiuojamas vieną kartą. Kampe esantis logotipas ir neryškus viso puslapio vandens ženklas gali būti paimti iš to paties perimto objekto

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;

Dvi techninės detalės padaro šį procesą saugų. Pirma, įterptas puslapio objektas priklauso puslapiui, o ne „XObject“. Vėliau atlaisvinus (freeing) „XObject“, jau atlikti įterpimai išlieka galiojantys. Būtent tai leidžia veikti žemiau aprašytai „sukurti-įterpti-atlaisvinti“ tvarkai. Antra, įterpimas ir pozicionavimas keičia puslapio objektų sąrašą tik atmintyje; UpdatePage yra tai, kas serijizuoja šį sąrašą atgal į puslapio turinio srautą (content stream), todėl puslapis, kurį redaguojate neiškvietę šios funkcijos, bus išsaugotas taip, tarsi spaudas niekada nebūtų buvęs uždėtas

Rodyklės (handle) gyvavimo trukmės taisyklė, kuri sukelia problemų

„XObject“ rodyklei (handle) taikomi du apribojimai, ir ignoruojant bet kurį iš jų kyla klaida, kuri atrodo nesusijusi su jos priežastimi. Pirma, šaltinio dokumentas turi būti aktyvus tuo metu, kai iškviečiate CreateXObjectFromPage. Perėmimas skaito šaltinio puslapio turinį iš atidaryto šaltinio dokumento, todėl tas dokumentas ir jo puslapis turi būti atidaryti ir galiojantys (valid) kuriant rodyklę. Antra, ir tai yra tai, kas stebina žmones, rodyklė turi būti atlaisvinta prieš uždarant šaltinio puslapį, o praktikoje – prieš uždarant ar atlaisvinant (freeing) šaltinio dokumentą, iš kurio ji kilusi

Priežastis yra ta, kad „XObject“ yra nuoroda į struktūrą, kuri vis dar priklauso šaltinio dokumentui. Tai nėra atsieta, savarankiška kopija, kurią galite nešiotis dingus šaltiniui. Pirmiausia uždarius šaltinį, rodyklė paliekama rodyti į turinį, kuris buvo sunaikintas, todėl vėlesnis jos atlaisvinimas ar bet koks kitas jos naudojimas veikia atmintyje, kuri nebegalioja (no longer valid). Simptomas yra klasikinis, būdingas „kabančioms“ rodyklėms (dangling handle): prieigos pažeidimas (access violation) išjungiant programą arba protarpinis gedimas, kuris kinta priklausomai nuo atminties paskirstymo tvarkos (allocation order), o klaidos steko (stack) nuoroda veda į valymo kodą, bet ne į eilutę, kuri iš tikrųjų sukėlė problemą. Sprendimas yra tvarka, o ne gynybinis kodavimas (defensive coding). Sukurkite „XObject“, įterpkite jį į kiekvieną puslapį, kuriam to reikia, atlaisvinkite „XObject“, ir tik tada uždarykite šaltinio dokumentą. TPdfXObject destruktorius už jus atlaisvina pagrindinę PDFium rodyklę, todėl laiku atlaisvinti ovjektą (wrapper) yra visa jūsų atsakomybė

Matrica ir ką reiškia jos šeši skaičiai

Įterpimas yra 2D afininė transformacija (affine transform), tokia pati, kokią PDF visur naudoja turinio pozicionavimui (ISO 32000-1, 8.3.4 skirsnis). Ją sudaro šeši skaičiai, užrašomi a, b, c, d, e, f, ir PDFium juos pateikia kaip FS_MATRIX įrašą (record). Jie susieja tašką iš objekto nuosavos erdvės į puslapio erdvę:

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

Šias šešias reikšmes galite įvesti rankiniu būdu, tačiau komponuojant jas rankiniu būdu kyla problemų su rotacija, nes rotacija sumaišo visus keturis kintamuosius a, b, c, d. Objekto „apvalkalas“ TPdfMatrixFPdfMatrix modulio sudaro įprastas operacijas už jus ir pritaiko daugybą (post-multiplies) eigoje, todėl Translate, Scale ir Rotate susijungia grandine ta tvarka, kuria jas iškviečiate. Įstrižas vandens ženklas yra pasukimas (rotate), po kurio eina perkėlimas (translate), skirtas percentruoti; kampe esantis logotipas yra mastelio keitimas (scale), po kurio eina perkėlimas (translate). Kai matrica bus paruošta, nukopijuokite jos neapdorotą (raw) reikšmę, FS_MATRIX tipo Handle savybę, į vietinį kintamąjį ir perduokite ją į FPDFPageObj_SetMatrix; importas deklaruoja matricą kaip var parametrą, todėl savybės (property) negalima jai perduoti tiesiogiai, o nesėkmės atveju jos rezultatas yra 0. Žemesnio lygio funkciją FPDFPageObj_Transform, kuri paima šešias reikšmes tiesiogiai kaip „doubles“, galima naudoti, kai mieliau norite perduoti skaičius, nei kurti „apvalkalą“ (wrapper)

Kiekvieno puslapio spaudavimas, teisinga tvarka

Pilnas šablonas sujungia visas dalis laikydamasis tvarkos, kurios reikalauja gyvavimo trukmės taisyklė. Atidarykite abu dokumentus, vieną kartą perimkite spaudą, pereikite per tikslinius puslapius iš eilės nustatydami nuo 1 pradedamą PageNumber ir įterpdami bei pozicionuodami kopiją, patvirtindami kiekvieną puslapį su UpdatePage, tuomet atlaisvinkite „XObject“, po to išsaugokite naudodami SaveAs ir galiausiai leiskite užsidaryti šaltinio dokumentui

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 blokų struktūra atlieka pagrindinį darbą. Vidinis finally atlaisvina „XObject“ prieš kontrolei (control) kada nors pasiekiant išorinį finally, kuris atlaisvina Stamp, todėl rodyklė (handle) visada paleidžiama, kol jos šaltinis vis dar gyvas (alive), net jei ciklo viduryje iškyla klaida. Teisingai sutvarkykite šį įdėjimą (nesting), ir gyvavimo trukmės taisyklė susitvarkys pati

Spaudavimas yra tik viena didesnio įrankių rinkinio, skirto puslapio turiniui kurti ir redaguoti, dalis. Jei jūsų spaudas pats savaime yra vaizdas, o ne perimtas puslapis, vaizdų konvertavimas į PDF dokumentus su PDFium apima tai, kaip iš pradžių įkelti šią taškinę grafiką į dokumentą. O kai dalykas, kurį norite neštis kartu su matomu spaudu, yra failas, o ne rašalas puslapyje, darbas su PDF priedais Delphi aplinkoje parodo įterpto failo pusę. Visa tai pateikiama su PDFium komponentu, skirtu Delphi ir C++Builder, kartu su atvaizdavimo (rendering), redagavimo ir dokumentų API, aptartais kitose šio tinklaraščio dalyse