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“ TPdfMatrix iš FPdfMatrix 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