Articol tehnic

Importul vectorilor EMF și WMF în PDF-uri Delphi cu HotPDF

HotPDF, componenta PDF nativă pentru Delphi și C++Builder, importă metafișierele Windows EMF și WMF interpretând fiecare înregistrare GDI direct în operatori PDF, în loc să reducă fișierul la un bitmap: umplerile cu gradient devin modele de umbrire axială PDF, periile de hașură devin modele de placare (tiling) PDF, iar o poartă centralizată de stare a traseului blochează înregistrările malformate să corupă rezultatul. Orice grafic pe care un TChart, o suprafață GDI+ sau un simplu TCanvas îl poate exporta ca metafișier îmbunătățit este un candidat pentru această cale, iar diferența se vede din momentul în care cineva mărește pagina sau o trimite la o imprimantă de rezoluție înaltă

Alternativa la care majoritatea dezvoltatorilor Delphi recurg implicit este rasterizarea metafișierului într-un bitmap înainte de a-l plasa pe pagină, iar costul apare abia mai târziu: un grafic cu bare care era clar pe ecran devine vizibil pixelat de îndată ce PDF-ul este tipărit la 600 DPI sau proiectat pe un ecran de sală de ședințe, iar o regiune CAD umplută cu hașură se prăbușește într-un simplu dreptunghi gri uniform dacă stilul de umplere nu este păstrat. Citirea metafișierului ca program, nu ca imagine, este ceea ce evită ambele probleme, și este calea mai grea de implementat corect, motiv pentru care capcanele de mai jos merită cunoscute înainte ca un raport să fie livrat

De ce interpretăm un metafișier în loc să-l reducem la un bitmap?

HotPDF păstrează importul EMF și WMF pe calea vectorială pentru că un metafișier Windows este o secvență înregistrată de apeluri de desenare GDI, nu o imagine, iar redarea acelor apeluri ca operatori PDF de traseu, text și umbrire este ceea ce permite rezultatului să se scaleze la fel ca restul paginii. THPDFPage.ShowMetafile și corespondentul său ShowMetafileEx sunt punctele de intrare pe care le apelează o aplicație, iar ambele predau metafișierul către THPDFWmf, clasa care parcurge fiecare înregistrare GDI și o traduce. Distincția nu este absolută, iar HotPDF nu pretinde altfel: o înregistrare de metafișier care este cu adevărat date raster, de exemplu un blit de bitmap StretchDIBits, este încorporată ca un obiect XObject de imagine PDF real prin AddImage și ShowImage, aceeași pereche de apeluri prin care trece orice altă imagine de pe pagină, în loc să fie forțată în operatori de traseu care nu pot exprima o fotografie. Liniile, umplerile și textul rămân vectoriale; pixelii care erau deja pixeli în sursă rămân pixeli în rezultat. Cel mai simplu apel nu necesită nimic în plus față de metafișierul încărcat:

var
  Pdf: THotPDF;
  Chart: TMetafile;
begin
  Pdf := THotPDF.Create(nil);
  Chart := TMetafile.Create;
  try
    Chart.LoadFromFile('quarterly-revenue.emf');  // exported from TChart or GDI+
    Pdf.FileName := 'quarterly-report.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafile(Chart);
    Pdf.EndDoc;
  finally
    Chart.Free;
    Pdf.Free;
  end;
end;

Cum transformă interpretorul coordonatele GDI în spațiul paginii PDF?

HotPDF răspunde la asta printr-o singură trecere peste propriul flux de înregistrări al metafișierului, în loc de o a doua implementare a GDI. THPDFWmf.Analyse citește antetul metafișierului prin apelul Win32 GetEnhMetaFileHeader, resetează starea sa internă de desenare și apelează EnumEnhMetafile, același API de enumerare pe care l-ar folosi un vizualizator de metafișiere, astfel încât fiecare înregistrare EMR_* ajunge la THPDFWmf.ExecuteRecord în ordinea în care a fost înregistrată inițial. GDI exprimă coordonatele de sus în jos, în unități de dispozitiv sau logice alese de propriul mod de mapare al metafișierului; o pagină PDF este de jos în sus, în puncte de spațiu-utilizator, sistemul de coordonate acoperit în modelul de desenare canvas al HotPDF pentru trasee și umpleri. Fiecare handler de înregistrare rezolvă această discrepanță prin ScaleX și ScaleY, care apelează ProjectX și ProjectY pentru a reda propria formulă GDI de conversie fereastră-la-viewport pentru modurile de mapare anizotropă și izotropă, astfel încât o formă înregistrată cu o lățime de cinci unități logice ajunge la lățimea corectă în puncte PDF, indiferent de ce extinderi de fereastră și viewport a stabilit aplicația sursă

Cum devine o umplere cu gradient GDI un model de umbrire PDF?

O înregistrare EMR_GRADIENTFILL devine un model real de umbrire axială PDF Type 2 (ISO 32000-1 §8.7.4.5) ori de câte ori GDI a înregistrat-o într-unul din cele două moduri de dreptunghi. THPDFWmf.VEMRGradientFill citește propriul aspect al înregistrării direct din bufferul brut de octeți, urmând structura MS-EMF §2.3.1.6: un tablou de vârfuri cu colțuri RGBA pe 16 biți, urmat de o listă de dreptunghiuri, fiecare referind două dintre acele vârfuri. Pentru GRADIENT_FILL_RECT_H, culorile trec de la stânga la dreapta de-a lungul liniei mediane orizontale a dreptunghiului; pentru GRADIENT_FILL_RECT_V, ele trec de sus în jos de-a lungul liniei mediane verticale. În orice caz, cele două culori de colț și coordonatele proiectate ale dreptunghiului merg direct în THotPDF.RegisterAxialGradient, care returnează un nume de model, iar pagina desenează dreptunghiul și îl umple prin acel model (SetFillPattern) în loc de un simplu apel SetRGBFillColor, astfel încât un antet cu benzi în stil foaie de calcul sau o zonă de grafic cu gradient își păstrează amestecul, în loc să se reducă la o singură culoare medie

Modul de triunghi Gouraud este golul onest. Când câmpul ulMode al înregistrării raportează GRADIENT_FILL_TRIANGLE, VEMRGradientFill îl recunoaște, înregistrează în jurnal faptul că modul de triunghi nu este încă implementat și sare peste dreptunghi în loc să ghicească o aproximare cu două culori. Interpolarea per-vârf, per-pixel pe o rețea arbitrară de triunghiuri nu se reduce la o umbrire axială sau radială cu două opriri de culoare, iar exprimarea corectă a acesteia ar însemna emiterea unei umbriri de plasă (mesh) PDF Type 4 sau Type 5, aceeași familie de umbrire pe care renderer-ul de pagini al HotPDF o lasă de asemenea nedesenată la citirea înapoi a unui PDF. Două căi de cod fără legătură între ele ajung la aceeași limită: umbririle de plasă sunt golul atât pe partea de scriere, cât și pe cea de citire, iar o diagramă sursă care folosește triunghiuri Gouraud pentru o strălucire radială fină revine la orice a fost ultima perie solidă, nu la o aproximare redată

Periile de hașură devin modele de placare, nu gri aplatizat

O perie de hașură GDI își păstrează textura în PDF pentru că THPDFWmf.SetBrushColor verifică CurrentBrush.lbStyle pentru BS_HATCHED înainte de a reveni vreodată la o umplere solidă, direcționând acel caz către SetHatchBrushPattern. Această metodă scrie un flux de conținut PDF de 8-pe-8 unități, format din operatori de linie trasată, m, l și S, aleși în funcție de stilul de hașură GDI: o singură linie orizontală sau verticală pentru HS_HORIZONTAL și HS_VERTICAL, trei diagonale paralele pentru HS_FDIAGONAL și HS_BDIAGONAL, și combinațiile orizontal-plus-vertical sau ambele diagonale pentru HS_CROSS și HS_DIAGCROSS. THotPDF.RegisterTilingPattern înregistrează acel flux de conținut ca un model de placare colorat (PaintType 1, ISO 32000-1 §8.7.3.1) cu XStep și YStep de 8 unități, iar pagina umple prin SetFillPattern la fel cum o face o umbrire axială. Un plan de etaj CAD sau un desen de inginerie care se bazează pe umpleri cu hașură pentru a distinge materialele își păstrează acel limbaj vizual în PDF, în loc să piardă fiecare regiune într-un gri identic

Nu fiecare perie primește acest tratament, iar golul merită cunoscut înainte ca un import CAD să fie livrat. EMR_CREATEDIBPATTERNBRUSHPT, înregistrarea pentru o perie de model cu imagine bitmap personalizată, nu unul din cele șase stiluri de hașură predefinite ale GDI, doar înregistrează handle-ul său, astfel încât înregistrările ulterioare SELECTOBJECT și DELETEOBJECT rămân consistente; HotPDF nu expune încă un pipeline de resurse PDF Pattern pentru imagini de placare arbitrare, așa că selectarea acelei perii cade înapoi la o umplere de rezervă cu culoare solidă, în loc de textura sursă. Dacă o umplere se redă plat acolo unde originalul a folosit clar o textură de imagine repetitivă, peria sursă este aproape sigur un model DIB personalizat, nu o hașură standard, și acesta este singurul caz care merită verificat manual mai întâi. Configurarea unui import pentru un asemenea desen trece în continuare prin același obiect de opțiuni:

var
  Pdf: THotPDF;
  Drawing: TMetafile;
  Options: THPDFEmfOptions;
begin
  Pdf := THotPDF.Create(nil);
  Drawing := TMetafile.Create;
  Options := THPDFEmfOptions.Create;
  try
    Drawing.LoadFromFile('floor-plan.emf');
    Options.Assign(Pdf.EmfOptions);   // start from the document-wide defaults
    Options.Redraw := False;          // interpret the original EMF bytes, no GDI re-record pass
    Options.ShowNullBrush := True;    // keep explicitly unfilled CAD regions visible
    Options.UseFrame := True;         // clip output to the frame the EMF header declares
    Pdf.FileName := 'floor-plan.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
    Pdf.EndDoc;
  finally
    Options.Free;
    Drawing.Free;
    Pdf.Free;
  end;
end;

Ce împiedică un metafișier malformat să corupă pagina?

Răspunsul HotPDF este o singură poartă în vârful ExecuteRecord, nu o verificare defensivă repetată în fiecare din cele aproximativ optzeci de handlere de înregistrări. O paranteză de traseu GDI, deschisă de EMR_BEGINPATH și închisă de EMR_ENDPATH sau EMR_ABORTPATH, este urmărită printr-o proprietate privată PathContinue, susținută de câmpul FPathContinue. Cât timp acea paranteză este deschisă, ExecuteRecord permite să treacă doar înregistrările de construire a traseului, variantele de mutare, linie, polilinie, poligon, polybezier și polydraw, plus CLOSEFIGURE și un mic set de înregistrări de transformare și stare DC precum SETWORLDTRANSFORM, SAVEDC și RESTOREDC. Orice alt tip de înregistrare care ajunge la ExecuteRecord cât timp paranteza este deschisă, de exemplu un EXTTEXTOUT rătăcit sau un blit de bitmap, este eliminat centralizat printr-un singur Exit în clipa în care sosește

Această poartă există pentru că o paranteză de traseu într-un metafișier scris manual, generat de un instrument sau pur și simplu corupt nu are garanția de a conține doar ceea ce un fișier bine format ar plasa între înregistrările sale de deschidere și închidere. O înregistrare de ieșire de text care ajunge între EMR_BEGINPATH și EMR_ENDPATH ar face, fără o poartă, fie să polueze geometria traseului în construcție, fie să emită un operator PDF de afișare text în mijlocul unei secvențe care ar trebui să fie construcție pură de traseu, iar ambele moduri de eșec sunt genul care apare pe o intrare malformată dintr-un instrument terț, nu pe ceva ce o suită de teste normală acoperă de obicei. Centralizarea verificării în ExecuteRecord înseamnă că handlerele individuale VEMR* nu trebuie fiecare să se apere împotriva faptului de a fi apelate la momentul greșit; poarta decide asta o singură dată, înainte de dispecerizare, în loc de optzeci de ori după aceea

Plasarea unui grafic vectorial lângă text și imagini pe o singură pagină

O pagină de raport rareori conține doar un grafic, iar ShowMetafile se combină cu ceilalți operatori de pagină ai HotPDF exact așa cum o face orice alt apel de desenare. Un titlu desenat cu TextOut, un grafic cu bare umplut cu hașură importat ca EMF și un logo plasat cu ShowImage pot ajunge toate pe aceeași pagină, în același flux de conținut, fiecare păstrându-și fidelitatea nativă, tiparul de compunere acoperit în ghidul HotPDF pentru amplasarea textului, fonturilor și imaginilor într-un raport:

Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart);   // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);

Interpretorul EMF și WMF, modelele de umbrire axială pe care le înregistrează pentru umplerile cu gradient și maparea pe modele de placare pentru periile de hașură descrise aici sunt livrate toate ca parte a componentei HotPDF standard pentru Delphi și C++Builder, o bibliotecă VCL nativă fără nicio dependență de DLL extern pentru toate acestea