Articol tehnic

Previzualizare tipărire și ieșire pe Device Context în Delphi cu PDF Library for Delphi

Randarea unei pagini PDF pe un context de dispozitiv Windows pentru previzualizarea tipăririi pune trei sisteme de coordonate în aceeași linie de cod, iar ele rareori cad de acord. Pagina PDF este măsurată în puncte, cu originea în colțul din stânga jos. DC-ul de ecran este măsurat în pixeli, cu originea în colțul din stânga sus și cu un factor de zoom pe care îl alegi tu. DC-ul de imprimantă, cel pe care previzualizarea ar trebui să îl anticipeze, măsoară pixeli la rezoluția dispozitivului, dar își plasează originea în colțul zonei imprimabile, nu în colțul colii. Greșește oricare dintre ele și previzualizarea arată bine, în timp ce pagina tipărită iese deplasată, scalată aiurea sau tăiată pe o margine. Simptomul obișnuit este un formular cu chenar care se previzualizează centrat și se tipărește cu liniile de sus și din stânga retezate, fiindcă imprimanta laser nu poate pune cerneală în ultimii câțiva milimetri de la margine și nimeni nu i-a spus previzualizării. losLab PDF Library (PDF Library for Delphi) acoperă tot traseul cu apeluri de randare pe context de dispozitiv, un strat de configurare a imprimantei virtuale și bitmap-uri de previzualizare generate din metricile proprii ale imprimantei, iar aceasta din urmă este partea care face previzualizarea sinceră în privința acelei margini

Geometria colii nu este geometria imprimabilă

Orice țintă de tipărire este descrisă de două dreptunghiuri, iar decalajul dintre ele este locul unde trăiesc cele mai multe erori de previzualizare. Dreptunghiul hârtiei este coala fizică. Dreptunghiul imprimabil este regiunea mai mică pe care motorul de tipărire chiar o poate atinge, retrasă cu o margine hardware ce diferă de la un model de imprimantă la altul și uneori de la o tavă la alta. Stratul de tipărire al bibliotecii le măsoară pe amândouă. Clasa TPLPrinter din spate expune PageWidth și PageHeight pentru zona imprimabilă, FullPageWidth și FullPageHeight pentru coala întreagă, plus PrintOffsetX împreună cu PrintOffsetY pentru distanța dintre originile lor, toate în pixeli de dispozitiv, la rezoluția pe care o raportează GetDPI. O previzualizare sinceră scalează aceleași numere până la rezoluția ecranului, în loc să picteze pagina în orice dreptunghi se întâmplă să aibă controlul. Sari peste acest pas și previzualizarea presupune tacit o margine zero, adică singura valoare pe care nicio imprimantă reală nu o folosește

Diagramă PDF Library for Delphi a foii de hârtie complete versus dreptunghiul mai mic imprimabil, cu PrintOffsetX și PrintOffsetY marcând marginea hardware dintre originile lor
Dreptunghiul hârtiei este foaia fizică, în timp ce dreptunghiul imprimabil este ce poate ajunge motorul de imprimare, iar decalajul dintre originile lor este locul unde trăiesc majoritatea bug-urilor de previzualizare

Previzualizare pe ecran prin RenderPageToDC

Pentru un control de previzualizare pe ecran, RenderPageToDC(DPI, Page, DC) desenează o pagină a documentului încărcat direct pe orice context de dispozitiv GDI, fie că este pânza unui TPaintBox, un bitmap din afara ecranului sau un DC de metafișier. Argumentul DPI stabilește zoom-ul. 96 aproximează o vizualizare la 100% pe un afișaj clasic, iar dublarea lui dublează dimensiunea randată

procedure TPreviewForm.PreviewBoxPaint(Sender: TObject);
begin
  // acestea trei sunt stare persistentă a bibliotecii, nu parametri per apel:
  FPdf.SetRenderDCOffset(FOffsetX, FOffsetY);
  FPdf.SetRenderDCErasePage(1);
  FPdf.SetRenderCropType(0);
  FPdf.RenderPageToDC(FPreviewDpi, FCurrentPage, PreviewBox.Canvas.Handle);
end;

Capcana este că traseul de randare pe DC este dirijat de stare persistentă a bibliotecii, nu de parametri per apel. SetRenderDCOffset, SetRenderDCErasePage și SetRenderCropType persistă fiecare până când ceva le schimbă, așa că o buclă de miniaturi care rulează după ce utilizatorul a ajustat vizualizarea mărită moștenește orice decalaj sau decupaj a lăsat în urmă traseul de cod anterior. Simptomul este o previzualizare care derivează doar în anumite secvențe de navigare, adică pe cât de chinuitor de reprodus poate fi un defect. Setarea întregii stări relevante la începutul handler-ului de desenare, ca mai sus, nu costă nimic și șterge toată clasa de probleme. Un al doilea multiplicator se ascunde prin apropiere. Rezoluția efectivă de ieșire este scara de randare înmulțită cu argumentul DPI, iar deși SetRenderScale are implicit valoarea 1.0, și ea persistă odată schimbată, așa că o funcție de export care a urcat-o rescalează pe tăcute fiecare previzualizare ulterioară până când ceva o pune la loc

Vizualizatoarele cu derulare și redesenările parțiale au o variantă dedicată. RenderPageToDCClip primește o specificație de decupare alături de contextul de dispozitiv, așa că invalidarea unei singure benzi din fereastră redesenează doar acea bandă, în loc să rasterizeze din nou pagina întreagă. La zoom mare, pe pagini de format mare, aceasta este diferența dintre un vizualizator care ține pasul cu bara de derulare și unul care se întinde în urma ei

O lucrare de tipărire care se potrivește cu previzualizarea

Partea de tipărire lucrează printr-o imprimantă virtuală. NewCustomPrinter clonează o imprimantă de sistem într-o configurație privată a bibliotecii, iar SetupPrinter ajustează acea clonă fără să atingă DevMode-ul valabil pe toată mașina: hârtia intră ca setarea 1 (o constantă DMPAPER_*), iar orientarea ca setarea 11. Câștigul este izolarea. Un serviciu poate tipări etichete A4 în timp ce imprimanta implicită a gazdei rămâne pe Letter, și nu trebuie restaurat nimic după aceea

PDF Library for Delphi: flux de la imprimanta implicită a sistemului prin NewCustomPrinter și SetupPrinter către un job de tipărire izolat care nu atinge niciodată DevMode-ul la nivel de mașină
SetupPrinter retargetează o clonă privată a bibliotecii, astfel încât un serviciu poate imprima A4 în timp ce imprimanta implicită a gazdei își păstrează DevMode Letter neatins
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);        // setarea 1 = hârtie, DMPAPER_A4
    Pdf.SetupPrinter(Virt, 11, 1);       // setarea 11 = orientare, 1 = portret
    Opt := Pdf.PrintOptions(1, 1, 'Monthly Report');  // încadrare în hârtie, rotire automată + centrare
    Pdf.PrintDocument(Virt, 1, Pdf.PageCount, Opt);
  finally
    Pdf.Free;
  end;
end;

PrintOptions merită citită cu atenție. Întoarce un handle de opțiuni pe care trebuie să îl transmiți lui PrintDocument sau PrintPages; nu este stare ambientală. Construirea opțiunilor urmată de uitarea handle-ului la transmitere eșuează în tăcere. Lucrarea se tipărește cu valorile implicite și nimeni nu observă până când se aștepta o politică de încadrare în hârtie, iar o pagină supradimensionată a ieșit tăiată. Argumentul de scalare a paginii este locul unde stă acea politică. Fără scalare se păstrează acuratețea dimensională, ceea ce contează pentru formularele care ajung măsurate cu rigla. Încadrarea în hârtie rescalează totul la coală. Micșorarea paginilor mari lasă paginile normale în pace și intervine doar când o pagină depășește zona imprimabilă, ceea ce este de obicei valoarea implicită potrivită pentru un set mixt de documente. Indicatorul de rotire automată și centrare rezolvă paginile în format vedere fără un al doilea traseu de cod

Aplicațiile care gestionează deja un TPrinter prin fluxul de dialog VCL îl pot preda direct. PrintDocumentToPrinterObject și PrintPagesToPrinterObject acceptă instanța TPrinter configurată, ceea ce păstrează dialogul standard de tipărire drept suprafață de configurare pentru utilizator, în timp ce biblioteca se ocupă de randarea paginilor. Amestecarea celor două abordări într-un singur traseu de cod tinde să reintroducă exact deriva de geometrie pe care restul acestei munci trebuia să o ucidă, deci alege una. Ruta imprimantei virtuale se potrivește serviciilor nesupravegheate; ruta TPrinter se potrivește aplicațiilor interactive

Ieșirea selectivă funcționează la fel. PrintPages primește un șir de interval, deci transmiterea numelui imprimantei virtuale, a lui '2-5,12' și a handle-ului de opțiuni tipărește paginile de la 2 la 5 și pagina 12 cu contractul de geometrie intact, iar aceeași sintaxă conduce și variantele de tipărire în fișier. Acele variante în fișier sunt răspunsul practic pentru un mediu nesupravegheat, fără dispozitiv fizic atașat: testarea de regresie a geometriei de tipărire pe un server de build care nu are deloc coadă de driver. Randează același document prin aceleași opțiuni într-un artefact de tip fișier la fiecare build, și o regresie de geometrie se transformă într-o diferență de fișiere, nu într-o sesizare de la client trei săptămâni mai târziu

Bitmap-uri de previzualizare cu metricile proprii ale imprimantei

O previzualizare randată la 96 DPI pe baza unei dimensiuni de pagină presupuse răspunde la întrebarea greșită. Ea arată cum arată pagina, nu ce va pune această imprimantă pe această hârtie. GetPrintPreviewBitmapToString închide golul construind previzualizarea din aceeași imprimantă personalizată și din același handle de opțiuni ca lucrarea de mai târziu, astfel încât dimensiunea hârtiei, orientarea, politica de scalare, rotirea și decalajul hardware intră toate în bitmap. Ce se întoarce este ceea ce va arăta coala

PDF Library for Delphi: contrast între o previzualizare pe ecran cu mărime de pagină presupusă, care deformează marginile, și un bitmap fidel imprimantei construit din handle-urile proprii de imprimantă și opțiuni ale jobului
GetPrintPreviewBitmapToString desenează previzualizarea din același handle de imprimantă personalizată și opțiuni ca jobul final, astfel încât bitmap-ul arată marginile și rotația pe care foaia le va primi cu adevărat
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;

Argumentul MaxDimension limitează latura lungă a bitmap-ului. 1200 de pixeli rămân clari pentru un dialog de previzualizare și țin memoria modestă chiar și pentru desene tehnice de format E, unde o randare la rezoluție completă, la cei 600 DPI ai imprimantei, ar ajunge la gigaocteți

Memorarea alegerilor de imprimantă ale utilizatorului

Dialogurile de tipărire care își uită setările de la o sesiune la alta generează singure tichete de suport. Perechea DevMode, GetPrinterDevModeToString și SetPrinterDevModeFromString, serializează configurația completă de driver a unei imprimante într-un șir opac pe care îl poți pune deoparte în preferințele utilizatorului și îl poți restaura la sesiunea următoare, inclusiv opțiunile specifice driverului pe care niciun API generic nu se obosește să le modeleze. Reține imprimanta după nume, luat din GetPrinterNames, niciodată după indexul din listă. Ordinea indexurilor se schimbă de fiecare dată când o imprimantă este adăugată sau eliminată, așa că un index salvat arată pe tăcute spre dispozitivul greșit data viitoare când lista se mișcă. GetDefaultPrinterName acoperă rezerva pentru cazul în care dispozitivul memorat a dispărut cu totul

Selecția tăvii completează povestea persistenței. GetPrinterBins raportează sursele de hârtie pe care le expune un driver, ceea ce contează pentru fluxurile cu hârtie cu antet, unde prima pagină se trage din tava cu antet, iar restul din hârtie simplă. Este o politică pe care utilizatorii se așteaptă să o rețină aplicația alături de toate celelalte, iar o lucrare care aterizează pe hârtia greșită se citește ca un defect chiar și când fiecare octet din PDF era corect

Păstrează un singur motor la previzualizare și la tipărire

O ultimă decizie guvernează discret fidelitatea. Selecția motorului de randare se aplică atât destinațiilor de ecran, cât și celor de imprimantă, așa că tentația este să previzualizezi cu un motor rapid și să tipărești cu unul precis. Rezistă-i. Conducerea previzualizării și a lucrării prin motoare diferite reintroduce exact deriva de fidelitate pe care o previzualizare fidelă imprimantei a fost construită să o înlăture, și o face într-un fel care iese la iveală doar pe hârtie. Compromisurile dintre motorul încorporat, Cairo și PDFium sunt cântărite în randarea PDF cu mai multe motoare în Delphi; alege unul și folosește-l de ambele părți

Documentele prea mari ca să fie încărcate confortabil înainte de tipărire pot fi deschise prin traseul de acces direct descris în îmbinarea, divizarea și accesul direct la PDF-uri mari, care randează pagini pe un context de dispozitiv pornind de la un handle de fișier, fără să construiască arborele documentului. Referința completă a API-ului de tipărire se află pe pagina de produs losLab PDF Library for Delphi