Tehnički članak

PDFlibPas prikaz pre štampe i izlaz na kontekst uređaja u Delphi-ju

Renderovanje PDF stranice na Windows kontekst uređaja (device context) za prikaz pre štampe postavlja tri koordinatna sistema u istu liniju koda, a oni se retko podudaraju. PDF stranica se meri u tačkama sa početkom u donjem levom uglu. Ekranski DC se meri u pikselima sa početkom u gornjem levom uglu i faktorom zumiranja koji sami izaberete. Štampačev DC, onaj koji bi prikaz pre štampe trebalo da predvidi, meri piksele u rezoluciji uređaja, ali postavlja svoj početak u ugao oblasti za štampanje, a ne u ugao lista papira. Ako pogrešite u bilo kom od ovih koraka, prikaz pre štampe će izgledati u redu, dok će odštampana stranica ispasti pomerena, skalirana ili odsečena duž ivice. Uobičajeni simptom je obrazac sa ivicom koji u prikazu pre štampe izgleda centriran, ali se štampa sa odsečenim gornjim i levim linijama, jer laserski štampač ne može da nanese mastilo na spoljnih nekoliko milimetara, a to niko nije preneo prikazu pre štampe. losLab PDF Library (PDFlibPas) pokriva ceo ovaj proces pomoću poziva za renderovanje na kontekst uređaja, konfiguracionog sloja virtuelnog štampača i bitmapa prikaza pre štampe generisanih na osnovu metričkih podataka samog štampača, što je deo koji prikaz pre štampe čini preciznim u pogledu te margine

Geometrija papira nije geometrija oblasti za štampanje

Dva pravougaonika opisuju bilo koju ciljnu površinu za štampu, a pomeraj između njih je mesto gde se krije većina bagova u prikazu pre štampe. Pravougaonik papira predstavlja fizički list. Pravougaonik za štampu je manja regija koju mehanizam štampača zapravo može dohvatiti, uvučena za hardversku marginu koja se razlikuje u zavisnosti od modela štampača, a ponekad i od fioke za papir. Sloj za štampanje u biblioteci meri oba parametra. Osnovna klasa TPLPrinter izlaže PageWidth i PageHeight za oblast za štampanje, FullPageWidth i FullPageHeight za ceo list papira, kao i PrintOffsetX i PrintOffsetY za razmak između njihovih početnih tačaka, sve izraženo u pikselima uređaja na rezoluciji koju prijavljuje GetDPI. Precizan prikaz pre štampe skalira te iste brojeve na ekransku rezoluciju umesto da iscrtava stranicu u bilo koji pravougaonik koji kontrola trenutno ima. Ako preskočite taj korak, prikaz pre štampe će prećutno pretpostaviti marginu nula, što je jedina vrednost koju nijedan pravi štampač ne koristi

Ekranski prikaz pre štampe pomoću RenderPageToDC

Za ekransku kontrolu prikaza pre štampe, RenderPageToDC(DPI, Page, DC) crta stranicu učitanog dokumenta direktno na bilo koji GDI kontekst uređaja, bilo da je to platno TPaintBox-a, bitmapa van ekrana ili DC metafajla. DPI argument postavlja nivo zumiranja. Vrednost 96 približno odgovara prikazu od 100% na klasičnom ekranu, a njeno dupliranje udvostručuje veličinu renderovanog prikaza

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // these three are sticky library state, not per-call parameters:
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

Zamka je u tome što je putanja renderovanja DC-ja vođena perzistentnim stanjem biblioteke, a ne parametrima po pojedinačnom pozivu. SetRenderDCOffset, SetRenderDCErasePage i SetRenderCropType ostaju aktivni sve dok ih nešto ne promeni, pa petlja za sličice (thumbnails) koja se pokreće nakon što je korisnik prilagodio zumirani prikaz nasleđuje bilo koji pomeraj ili isecanje koje je prethodna putanja koda ostavila. Simptom je prikaz pre štampe koji odstupa samo u određenim redosledima navigacije, što je izuzetno teško reprodukovati. Postavljanje svih relevantnih stanja na vrhu rukovaoca crtanjem, kao što je prikazano iznad, ne košta ništa i eliminiše celu ovu klasu problema. Drugi multiplikator se krije u blizini. Efektivna izlazna rezolucija je razmera renderovanja pomnožena sa DPI argumentom, i iako je podrazumevana vrednost za SetRenderScale 1.0, ona takođe ostaje aktivna nakon promene, pa funkcija za izvoz koja ju je povećala tiho menja razmeru svakog kasnijeg prikaza pre štampe sve dok je nešto ne vrati na staro

Pregledači sa skrolovanjem i delimičnim ponovnim iscrtavanjem imaju namensku varijantu. RenderPageToDCClip prihvata specifikaciju isecanja (clip) zajedno sa kontekstom uređaja, tako da poništavanje (invalidating) jedne trake prozora ponovo iscrtava samo tu traku umesto ponovnog rasterizovanja cele stranice. Pri visokom zumiranju na stranicama velikog formata, to čini razliku između pregledača koji glatko prati klizač i onog koji kasni i razmazuje sliku

Posao štampanja koji odgovara prikazu pre štampe

Strana za štampanje radi preko virtuelnog štampača. NewCustomPrinter klonira sistemski štampač u privatnu konfiguraciju biblioteke, a SetupPrinter prilagođava taj klon bez uticanja na DevMode na nivou celog sistema: papir se podešava preko parametra 1 (DMPAPER_* konstanta), a orijentacija preko parametra 11. Rezultat je izolacija. Servis može da štampa A4 nalepnice dok podrazumevani štampač hosta ostaje na Letter formatu, i ništa ne mora da se vraća u prvobitno stanje nakon toga

var
  Pdf: TPDFlib;
  Virt: WideString;
  Opt: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('report.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Virt := Pdf.NewCustomPrinter(Pdf.GetDefaultPrinterName);
    Pdf.SetupPrinter(Virt, 1, 9);        // setting 1 = paper, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // setting 11 = orientation, 1 = portrait
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // fit to paper, auto-rotate + center
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

Funkciju PrintOptions treba pažljivo proučiti. Ona vraća hendl opcija koji morate proslediti funkciji PrintDocument ili PrintPages; to nije neko ambijentalno stanje. Kreiranje opcija i naknadno zaboravljanje da se prosledi hendl prolazi bez prijavljivanja greške. Posao se štampa sa podrazumevanim vrednostima, i niko to ne primećuje sve dok se ne desi da se očekuje uklapanje na papir (fit-to-paper), a prevelika stranica umesto toga bude odsečena. Argument za skaliranje stranice je mesto gde se nalazi to pravilo. Bez skaliranja čuva se dimenzionalna tačnost, što je važno za obrasce koji se mere lenjirom. Fit-to-paper skalira sve na veličinu lista. Shrink-large-pages ostavlja normalne stranice nepromenjenim i interveniše samo kada stranica premaši oblast za štampu, što je obično pravi izbor za mešoviti skup dokumenata. Zastavica za automatsko rotiranje i centriranje rukuje horizontalno orijentisanim (landscape) stranicama bez potrebe za dodatnim kodom

Aplikacije koje već upravljaju TPrinter objektom kroz VCL dijalog mogu ga direktno proslediti. PrintDocumentToPrinterObject i PrintPagesToPrinterObject prihvataju konfigurisanu instancu TPrinter-a, što omogućava da standardni dijalog za štampanje ostane kao korisnički konfiguracioni interfejs dok biblioteka rukuje renderovanjem stranice. Mešanje ova dva pristupa u istoj putanji koda obično ponovo uvodi odstupanja u geometriji koja smo ovim radom hteli da eliminišemo, pa odaberite samo jedan. Putanja virtuelnog štampača odgovara automatskim servisima bez korisničkog interfejsa; putanja TPrinter-a odgovara interaktivnim aplikacijama

Selektivno štampanje radi na isti način. PrintPages prihvata string sa opsegom stranica, pa prosleđivanje naziva virtuelnog štampača, opsega '2-5,12' i hendla opcija štampa stranice od 2 do 5 i stranicu 12 uz očuvanje geometrije, a ista sintaksa pokreće i varijante za štampanje u fajl. Te varijante za štampanje u fajl su praktično rešenje za automatizovana okruženja bez povezanog fizičkog uređaja: regresiono testiranje geometrije štampe na build serveru koji uopšte nema red za drajvere. Renderujte isti dokument sa istim opcijama u fajl artefakt pri svakom buildu, pa će se regresija geometrije pretvoriti u običnu razliku (diff) u fajlovima umesto u prijavu baga od strane korisnika tri nedelje kasnije

Bitmape prikaza pre štampe sa sopstvenim metričkim podacima štampača

Prikaz pre štampe renderovan u 96 DPI u odnosu na pretpostavljenu veličinu stranice daje odgovor na pogrešno pitanje. On pokazuje kako stranica izgleda uopšteno, a ne šta će ovaj konkretan štampač naneti na ovaj papir. Funkcija GetPrintPreviewBitmapToString premošćuje taj jaz tako što kreira prikaz pre štampe pomoću istog prilagođenog štampača i istog hendla opcija kao i za stvarni posao, pa se veličina papira, orijentacija, pravilo skaliranja, rotacija i hardverski pomeraj uzimaju u obzir prilikom generisanja bitmape. Ono što dobijete nazad je upravo ono što će se pojaviti na papiru

procedure ShowPrinterTruePreview(Pdf: TPDFlib; const Virt: WideString; Opt: Integer);
var
  Data: AnsiString;
  Strm: TMemoryStream;
  Bmp: TBitmap;
begin
  Data := Pdf.GetPrintPreviewBitmapToString(Virt, 1, Opt, 1200, 0);
  Strm := TMemoryStream.Create;
  try
    Strm.WriteBuffer(PAnsiChar(Data)^, Length(Data));
    Strm.Position := 0;
    Bmp := TBitmap.Create;
    try
      Bmp.LoadFromStream(Strm);
      PreviewImage.Picture.Assign(Bmp);
    finally
      Bmp.Free;
    end;
  finally
    Strm.Free;
  end;
end;

Argument MaxDimension ograničava dužu ivicu bitmape. Vrednost od 1200 piksela ostaje oštra za dijalog prikaza pre štampe i održava potrošnju memorije skromnom čak i za inženjerske crteže E-veličine, gde bi renderovanje u punoj rezoluciji na 600 DPI štampača zahtevalo gigabajte memorije

Pamćenje korisnikovog izbora štampača

Dijalozi za štampanje koji zaboravljaju svoja podešavanja između sesija generišu sopstvene tikete podrške. DevMode par funkcija, GetPrinterDevModeToString i SetPrinterDevModeFromString, serijalizuje kompletnu konfiguraciju drajvera štampača u neprozirni string koji možete sačuvati u korisničkim podešavanjima i vratiti u sledećoj sesiji, uključujući i opcije specifične za drajver koje nijedan generički API ne modelira. Sačuvajte štampač po nazivu dobijenom preko GetPrinterNames, nikada po indeksu liste. Redosled indeksa se menja svaki put kada se štampač doda ili ukloni, pa sačuvani indeks može tiho ukazati na pogrešan uređaj sledeći put kada se lista promeni. GetDefaultPrinterName pokriva rezervnu varijantu u slučaju da je zapamćeni uređaj potpuno nestao

Izbor fioke zaokružuje priču o perzistenciji. GetPrinterBins prijavljuje izvore papira koje drajver izlaže, što je važno za procese štampanja na memorandumu gde se prva stranica povlači iz fioke sa memorandumom, a ostatak iz obične fioke. To je ponašanje za koje korisnici očekuju da ga aplikacija zapamti zajedno sa svim ostalim, a posao štampanja koji završi na pogrešnom papiru smatra se bagom čak i kada je svaki bajt PDF-a bio ispravan

Koristite isti mehanizam za prikaz i štampanje

Jedna poslednja odluka tiho upravlja vernošću prikaza. Izbor mehanizma za renderovanje primenjuje se i na ekransku i na destinaciju štampača, pa se javlja iskušenje da se za prikaz pre štampe koristi brz mehanizam, a za štampanje precizan. Oduprite se tome. Pokretanje prikaza pre štampe i samog štampanja kroz različite mehanizme ponovo uvodi upravo ono odstupanje u vernosti koje je prikaz zasnovan na metrikama štampača trebalo da eliminiše, i to na način koji se vidi tek na papiru. Kompromisi između ugrađenog mehanizma, Cairo i PDFium mehanizma razmatrani su u članku o višestrukom renderovanju PDF-a u Delphi-ju; izaberite jedan i koristite ga na obe strane

Dokumenti koji su preveliki da bi se komotno učitali pre štampanja mogu se otvoriti kroz putanju za direktan pristup opisanu u članku o spajanju, deljenju i direktnom pristupu velikim PDF fajlovima, što renderuje stranice na kontekst uređaja direktno iz fajl hendla bez izgradnje stabla dokumenta. Kompletna referenca API-ja za štampanje nalazi se na stranici proizvoda losLab PDF Library for Delphi