Tehnički članak

HotPDF Resolution u Delphiju: crtaće jedinice i UserWidth

U HotPDF Component THotPDF.Resolution definira crtaću jedinicu: svaka X i Y koordinata, svaka margina, veličina predana SetFontu i rezultati TextWidtha i GetWideTextWidtha mjere se u 1/Resolution inča. THPDFPage.Width i Height ne slijede je i ostaju u točkama, pa granice rasporeda moraju doći iz read-only UserWidtha i UserHeighta. Uobičajen razlog za diranje Resolutiona jest port: engine izvještaja koji već misli u 1/96 ili 1/144 inča lakše se preseli kad PDF strana govori istu jedinicu, nego kad svaki call site dobije faktor pretvorbe. To radi dobro, dok god znate koji su se brojevi preselili u novu jedinicu, a koji su ostali

Što THotPDF.Resolution zapravo mijenja?

THotPDF.Resolution mijenja samo način na koji HotPDF čita brojeve koje predate; PDF koji zapiše isti je. Setter su dva retka: SetResolution pohrani vrijednost i postavi DocScale := Value / 72. Od tada XProjection i YProjection dijele svaku koordinatu s DocScaleom na putu u content stream, a SetFont dijeli veličinu isto prije nego je zabilježi. PDF user space zadan je na 1/72 inča (ISO 32000-1 §8.3.2.3), pa na zadanom Resolutionu 72 projekcija je identitet, a na 144 jedna crtaća jedinica je pola točke. Nijedan unos /UserUnit ne zapisuje se. To svojstvo stranice, dodano u PDF 1.6, zasebna je stvar koju HotPDF izlaže kao THPDFPage.SetUserUnit. Jedan detalj koji uhvati ljude koji dolaze s TextOut vodiča: koordinate stranice teku od gornjeg lijevog kuta s Y koji raste prema dolje, jer YProjection računa vrh MediaBoxa minus skalirani Y, i to vrijedi na svakom Resolutionu

Kako THotPDF.Resolution definira crtaću jedinicu u Delphiju: setter pohrani DocScale kao Resolution podijeljen s 72, pa XProjection, YProjection i SetFont dijele svaku koordinatu i veličinu na putu u content stream, tako da je Resolution 72 identitetno mapiranje, a Resolution 144 čini jednu crtaću jedinicu pola točke dok stranica i dalje teče od gore lijevo s Y prema dolje
Ništa u izlaznoj datoteci se ne pomakne — mijenja se samo značenje brojeva koje predajete, zato se isti content stream pojavljuje na 72 i 144
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Margin: Single;
  Title: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Resolution := 144;               // 1 crtaća jedinica = 1/144 inča
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // jedan inč u crtaćim jedinicama
    Page.SetFont('Arial', [fsBold], 28); // 28/144 inča, font od 14 pt
    Title := 'INVOICE 2026-0417';
    // Poravnajte desno uz rub stranice izmjeren u istoj jedinici
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // linija od 1 pt
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Zašto se Page.Width ne slaže s mojim koordinatama na Resolutionu 144?

THPDFPage.Width i Height javljaju stranicu u točkama ma što dokumentni Resolution bio, dok su vaše koordinate u 1/Resolution inča, pa na 144 stranica izgleda upola uža nego što jest. A4 stranica čita Width = 595 i Height = 842 na Resolutionu 72 i i dalje čita 595 i 842 na 144, gdje je desni rub zapravo na X = 1190. UserWidth i UserHeight, dodani u v2.766.0, vraćaju Width * DocScale, što je veličina stranice u jedinici kojom crtate. Prije njih biblioteka je interno miješala dvoje, i simptomi na Resolutionu 144 bili su dramatični: odlomci su se lomili iza svakog znaka, THPDFTable.Render gurao je svaki redak na novu stranicu, i i HTML uvoznik i XFA flattener crtali su sadržaj upola manje, spljoštena forma ugurana u gornji lijevi kut. Raspored odlomaka, renderiranje tablica, HTML uvoz, EMF centriranje, WMF clip stranice i dijagnostika rasporeda sada svi čitaju veličinu u user jedinicama. I vaš rasporedni kod treba isto: sve što uspoređuje s crtaćom koordinatom (desna margina, test prijeloma stranice, izračun centriranja) pripada na UserWidth i UserHeight, nikad na Width i Height

Zamka prva: dodjela Width ili Height prebacuje stranicu na točke

Postavljanje Page.Width ili Page.Height tiho prebaci stranicu na UserDefined, a UserDefined stranica potpuno ignorira DocScale, pa je sve što na nju poslije nacrtate u točkama, ne u 1/Resolution inča. Setter je star i po dizajnu prima točke, zato mu je značenje ostavljeno na miru. Projekcija za UserDefined stranicu je puki X + MinX, a SetFont pohranjuje veličinu nepromijenjenu. Na Resolutionu 144 rezultat je stranica čiji sadržaj odjednom izađe dvostruko veći nego na stranici prije nje. Biblioteka je napravila upravo ovu pogrešku sama: stranice nastavka odlomaka kopirale su veličinu prethodne stranice kroz Width, i svaka overflow stranica prebacivala se na točke. Te stranice sada kopiraju Size, Orientation i Resolution stranice, i vraćaju se na Width i Height samo kad je izvorna stranica već bila UserDefined

Dva izlaza, ovisno o tome što trebate. Ako će standardni list poslužiti, postavite Page.Size i Page.Orientation i nastavite crtati u svojoj Resolution jedinici. Ako stvarno trebate vlastitu veličinu stranice, prihvatite da je to stranica u točkama i crtajte u točkama; UserWidth je tamo jednak Widthu, pa rasporedni kod koji uvijek čita UserWidth i dalje radi na objema vrstama stranica. Unit test to pribija: na Resolutionu 144 A4 stranica javlja UserWidth od 1190, ali nakon Width := 500 i Height := 400 javlja 500 i 400. Učitane stranice ponašaju se isto, jer stranica ponovno izgrađena iz postojećeg PDF-a zna samo svoj MediaBox u točkama i crta u točkama. Stranice koje je ovaj dokument stvorio čuvaju vlastite jedinice kad prebacite van i vratite se kroz CurrentPageNumber, što je slučaj od v2.766.26

Zašto se Page.Width ne slaže s vašim koordinatama na Resolutionu 144 u HotPDF-u: Width i Height ostaju u točkama dok crtanje koristi 1/144 inča, pa A4 stranica čita 595 ali njezin desni rub leži na UserWidth 1190, a dodjela Width prebaci stranicu na UserDefined, koji ignorira DocScale, pa se odlomci lome po znaku, tablice po retku, a veličine SetFonta popola
Sve što se uspoređuje s crtaćom koordinatom pripada na UserWidth i UserHeight — na UserDefined točkovnoj stranici dvoje se poklopi, pa isti rasporedni kod preživi obje

Zamka druga: zašto veličine fontova izlaze upola manje?

Veličina fonta koja je počela kao točke izlazi upola manja na Resolutionu 144 jer SetFont svoj argument veličine tretira kao crtaće jedinice i pretvara ga u točke prije pohrane. Iznutra SetFont pohranjuje ASize / DocScale * DPI u trenutni font objekt, pa je pohranjena vrijednost uvijek točke. Biblioteka je o tome sapela dvaput: fontovski fallback u WideTextOutBoxExu i stranica nastavka odlomaka oboje su vratili tu pohranjenu točkovnu vrijednost u SetFont, koji ju je skalirao drugi put i upolaio tekst. Vaš kod ne može pročitati pohranjenu veličinu, ali isti bug iskoči svaki put kad točkovna vrijednost odnekud drugamo stigne do SetFonta: TFont.Size iz VCL forme, veličina u definiciji izvještaja, CSS pt duljina. Pretvorite je najprije, i u faktor uključite vlastiti Resolution stranice i slučaj UserDefined, kako čini playback metafila kad ponavlja Canvas stranice (vidite kako HotPDF uvozi EMF i WMF vektorsku grafiku za taj put):

// Crtaće jedinice po točki na trenutnoj stranici. Ogledala projekciju
// koju HotPDF koristi: 1 na stranici dimenzioniranoj kroz Width/Height, inače
// (document Resolution / 72) * (page Resolution / 72)
function UnitsPerPoint(Pdf: THotPDF): Single;
begin
  if Pdf.CurrentPage.Size = UserDefined then
    Result := 1
  else
    Result := (Pdf.Resolution / 72) * (Pdf.CurrentPage.Resolution / 72);
end;

procedure SetFontFromVcl(Pdf: THotPDF; Font: TFont);
begin
  // TFont.Size je u točkama; SetFont očekuje crtaće jedinice
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

Biblioteka primjenjuje isto pravilo na vlastite točkovne konstante. Font od 12 točaka s kojim kreće svaka nova stranica sada se množi internim faktorom jedinica-po-točki, pa je 12 točaka na svakom Resolutionu. DrawChart, čije su margine, veličine oznaka i debljine linija sve hardkodirane točke, sada radi sa skalom privremeno postavljenom na 1. Ono što ostaje u crtaćim jedinicama, namjerno, javni su zadani parametri poput veličine modula DrawQRCodea i zadanog fonta tablice: dio su API ugovora, pa na Resolutionu 144 znače polovicu onoga što znače na 72. Ako dimenzionirate izvještaje iz predloška, vodič izlazu izvještaja s fontovima i slikama u HotPDF-u pokriva odakle te vrijednosti obično dolaze

Kako provjerite da je raspored neovisan o Resolutionu?

Najpouzdanija je provjera usporedba bajtova: renderirajte istu stranicu na Resolutionu 72, pa opet na 144 s udvostručenom svakom koordinatom i veličinom, i nekompresirani se content streamovi moraju poklapati. Oba runa slijeću na iste točkovne vrijednosti iza projekcije, pa je svaka razlika vrijednost koja je preskočila pretvorbu. Tako test suite HotPDF-a provjerava odlomke, tablice, HTML uvoz, XFA spljoštavanje, lukove, metafile i slike. Ista tehnika radi za vaš vlastiti izvještajni kod uz gotovo nikakav testni okvir:

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // čitljivi content streamovi
    Pdf.FileName := FileName;
    Pdf.Resolution := Res;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 10 * K);
    Pdf.CurrentPage.TextOut(36 * K, 36 * K, 0, 'Line 1');
    Pdf.CurrentPage.Rectangle(36 * K, 60 * K, 200 * K, 40 * K);
    Pdf.CurrentPage.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

// RenderPage('r72.pdf', 72, 1) and RenderPage('r144.pdf', 144, 2)
// mora proizvesti bajtno identične content streamove stranice
Kako provjeriti neovisnost o Resolutionu u HotPDF Delphi kodu: renderirajte identičan raspored dvaput, jednom na Resolutionu 72 sa skalom 1 i jednom na 144 s udvostručenom svakom koordinatom i veličinom fonta, pa zahtijevajte bajtno identične nekompresirane content streamove — nepoklapanje upućuje na stranicu prebačenu na UserDefined kroz Width ili nepretvorenu točkovnu vrijednost koja stigne do SetFonta
Oba runa slijeću na iste točkovne vrijednosti iza projekcije, pa je svaka razlika broj koji je preskočio svoju pretvorbu — isti okvir na koji se oslanja test suite HotPDF-a

Provjerite operatore koji nose brojeve: Td, Tm, Tf, re, w i polja TJ. Bajtovi na razini datoteke i dalje će se razlikovati u datumu stvaranja i /IDu, pa uspoređujte streamove, ne cijele datoteke. Nepoklapanje gotovo uvijek upućuje na jednu od dviju zamki gore: stranicu promijenjene veličine kroz Width ili točkovnu vrijednost predanu ravno u SetFont. Ako ste novi u samim crtaćim pozivima, počnite s HotPDF TextOut proputovanjem za veličinu, stil i rotaciju, pa se vratite i prebacite Resolution jednom kad vaš raspored čita UserWidth. Potpune API pojedinosti i trial preuzimanja su na stranici HotPDF Delphi PDF komponente