Artykuł techniczny

HotPDF Resolution w Delphi: jednostki rysowania i UserWidth

W HotPDF Component THotPDF.Resolution definiuje jednostkę rysowania: każda współrzędna X i Y, każdy margines, rozmiar podawany do SetFont oraz wyniki TextWidth i GetWideTextWidth mierzone są w 1/Resolution cala. THPDFPage.Width i Height nie idą za tym i zostają w punktach, więc granice layoutu muszą pochodzić z tylko do odczytu UserWidth i UserHeight. Zwykły powód dotykania Resolution to port: silnik raportów, który już myśli w 1/96 albo 1/144 cala, łatwiej się przenosi, gdy strona PDF mówi tą samą jednostką, niż gdy każde miejsce wywołania dostaje współczynnik przeliczenia. To działa dobrze, dopóki wiesz, które liczby przeniosły się do nowej jednostki, a które zostały z tyłu

Co THotPDF.Resolution tak naprawdę zmienia?

THotPDF.Resolution zmienia tylko to, jak HotPDF czyta liczby, które mu podajesz; zapisywany PDF jest ten sam. Setter to dwie linie: SetResolution przechowuje wartość i ustawia DocScale := Value / 72. Od tego momentu XProjection i YProjection dzielą każdą współrzędną przez DocScale w drodze do strumienia treści, a SetFont dzieli rozmiar tak samo, zanim go zapamięta. Przestrzeń użytkownika PDF domyślnie to 1/72 cala (ISO 32000-1 §8.3.2.3), więc przy domyślnym Resolution 72 projekcja jest tożsamością, a przy 144 jedna jednostka rysowania to pół punktu. Żaden wpis /UserUnit nie jest pisany. Ten atrybut strony, dodany w PDF 1.6, to osobna rzecz, którą HotPDF wystawia jako THPDFPage.SetUserUnit. Szczegół, który łapie ludzi przychodzących z samouczków TextOut: współrzędne strony biegną od lewego górnego rogu, z Y rosnącym w dół, bo YProjection liczy górę MediaBoxa minus przeskalowane Y, i to zostaje prawdą przy każdym Resolution

Jak THotPDF.Resolution definiuje jednostkę rysowania w Delphi: setter przechowuje DocScale jako Resolution podzielone przez 72, po czym XProjection, YProjection i SetFont dzielą każdą współrzędną i rozmiar w drodze do strumienia treści, więc Resolution 72 to mapowanie tożsamościowe, a Resolution 144 czyni jedną jednostkę rysowania pół punktu, podczas gdy strona nadal biegnie od lewego górnego rogu z Y w dół
Nic w pliku wyjściowym się nie rusza — zmienia się tylko znaczenie liczb, które podajesz, dlatego ten sam strumień treści wychodzi przy 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 jednostka rysowania = 1/144 cala
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // jeden cal w jednostkach rysowania
    Page.SetFont('Arial', [fsBold], 28); // 28/144 cala, font 14 pt
    Title := 'INVOICE 2026-0417';
    // Wyrównanie do prawej względem krawędzi strony mierzonej tą samą jednostką
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // linia 1 pt
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Dlaczego Page.Width nie zgadza się z moimi współrzędnymi przy Resolution 144?

THPDFPage.Width i Height raportują stronę w punktach, cokolwiek mówi Resolution dokumentu, podczas gdy twoje współrzędne są w 1/Resolution cala, więc przy 144 strona wygląda na dwukrotnie węższą, niż jest. Strona A4 czyta Width = 595 i Height = 842 przy Resolution 72 i nadal czyta 595 i 842 przy 144, gdzie prawa krawędź faktycznie leży przy X = 1190. UserWidth i UserHeight, dodane w v2.766.0, zwracają Width * DocScale, czyli rozmiar strony w jednostce, którą rysujesz. Zanim powstały, biblioteka mieszała te dwie rzeczy wewnętrznie, a objawy przy Resolution 144 były widowiskowe: akapity łamały się po każdym znaku, THPDFTable.Render wypychał każdy wiersz na nową stronę, a importer HTML i spłaszczacz XFA rysowały swoją treść w połowie rozmiaru, spłaszczony formularz stłoczony w lewym górnym rogu. Łamanie akapitów, renderowanie tabel, import HTML, centrowanie EMF, klip strony WMF i diagnostyka layoutu czytają teraz wszystkie rozmiar w jednostce użytkownika. Twój własny kod layoutu powinien też: cokolwiek porównuje ze współrzędną rysowania (prawy margines, test łamania strony, obliczenie centrowania) należy na UserWidth i UserHeight, nigdy na Width i Height

Pułapka pierwsza: przypisanie Width albo Height przełącza stronę na punkty

Ustawienie Page.Width albo Page.Height po cichu zmienia stronę na UserDefined, a strona UserDefined ignoruje DocScale w całości, więc wszystko, co na niej potem narysujesz, jest w punktach, nie w 1/Resolution cala. Setter jest stary i przyjmuje punkty z założenia, dlatego jego znaczenie zostawiono w spokoju. Projekcja dla strony UserDefined to gołe X + MinX, a SetFont przechowuje rozmiar bez zmian. Przy Resolution 144 skutkiem jest strona, której treść nagle wychodzi dwa razy większa niż na stronie poprzedniej. Biblioteka popełniła dokładnie ten błąd sama: strony kontynuacji akapitów kopiowały kiedyś rozmiar poprzedniej strony przez Width i każda strona przepełnienia przełączała się na punkty. Te strony kopiują teraz Size, Orientation i Resolution strony, a do Width i Height spadają dopiero, gdy oryginalna strona już była UserDefined

Dwa wyjścia, zależnie od potrzeb. Jeśli standardowy arkusz wystarczy, ustaw Page.Size i Page.Orientation i rysuj dalej w swojej jednostce Resolution. Jeśli naprawdę potrzebujesz niestandardowego rozmiaru strony, zaakceptuj, że to strona punktowa, i rysuj w punktach; UserWidth równa się tam Width, więc kod layoutu zawsze czytający UserWidth działa dalej na obu rodzajach stron. Test jednostkowy przypina to: przy Resolution 144 strona A4 raportuje UserWidth 1190, ale po Width := 500 i Height := 400 raportuje 500 i 400. Wczytane strony zachowują się tak samo, bo strona przebudowana z istniejącego PDF-a zna tylko swój MediaBox w punktach i rysuje w punktach. Strony stworzone przez ten dokument trzymają własne jednostki, gdy się przełączysz i wrócisz przez CurrentPageNumber, i tak jest od v2.766.26

Dlaczego Page.Width nie zgadza się z twoimi współrzędnymi przy Resolution 144 w HotPDF: Width i Height zostają w punktach, podczas gdy rysowanie używa 1/144 cala, więc strona A4 czyta 595, ale jej prawa krawędź leży przy UserWidth 1190, a przypisanie Width przełącza stronę na UserDefined ignorującą DocScale, więc akapity łamią się po znaku, tabele po wierszu, a rozmiary SetFont spadają o połowę
Cokolwiek porównuje ze współrzędną rysowania, powinno siedzieć na UserWidth i UserHeight — na punktowej stronie UserDefined obie wielkości się pokrywają, więc ten sam kod layoutu przeżywa jedno i drugie

Pułapka druga: dlaczego rozmiary fontów wychodzą o połowę?

Rozmiar fontu, który zaczął jako punkty, wychodzi o połowę przy Resolution 144, bo SetFont traktuje swój argument rozmiaru jako jednostki rysowania i przelicza go na punkty przed zapisaniem. Wewnętrznie SetFont przechowuje ASize / DocScale * DPI w bieżącym obiekcie fontu, więc przechowywana wartość to zawsze punkty. Biblioteka potknęła się o to dwa razy: fallback fontu w WideTextOutBoxEx i strona kontynuacji akapitu obie oddawały tę zapisaną wartość punktową z powrotem do SetFont, które skalowało ją drugi raz i poławiało tekst. Twój kod nie może przeczytać zapisanego rozmiaru, ale ten sam błąd wychodzi za każdym razem, gdy wartość punktową z cudzej strony dociera do SetFont: TFont.Size z formularza VCL, rozmiar w definicji raportu, długość pt z CSS. Przelicz ją najpierw i wlicz do współczynnika własne Resolution strony oraz przypadek UserDefined, tak robi odtwarzanie metafile przy powtórce Canvas strony (ta ścieżka jest opisana w artykule o tym, jak HotPDF importuje grafikę wektorową EMF i WMF):

// Jednostki rysowania na punkt na bieżącej stronie. Odbija projekcję
// HotPDF: 1 na stronie wymiarowanej przez Width/Height, inaczej
// (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 jest w punktach; SetFont oczekuje jednostek rysowania
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

Biblioteka stosuje tę samą regułę do własnych stałych punktowych. Font 12-punktowy, od którego zaczyna się każda nowa strona, jest teraz mnożony przez wewnętrzny współczynnik jednostek-na-punkt, więc ma 12 punktów przy każdym Resolution. DrawChart, którego marginesy, rozmiary etykiet i grubości linii to wszystko zaszyte punkty, biegnie teraz z tymczasowo ustawioną skalą 1. W jednostkach rysowania, z premedytacją, zostają publiczne domyślne wartości parametrów, jak rozmiar modułu DrawQRCode i domyślny rozmiar fontu tabeli: one są częścią kontraktu API, więc przy Resolution 144 znaczą połowę tego, co znaczą przy 72. Jeśli wymiarujesz raporty z szablonu, przewodnik o wyjściu raportów z fontami i obrazami w HotPDF omawia, skąd te wartości zwykle pochodzą

Jak zweryfikować, że layout jest niezależny od Resolution?

Najbardziej niezawodne sprawdzenie to porównanie bajtów: wyrenderuj tę samą stronę przy Resolution 72, a potem przy 144 z każdą współrzędną i rozmiarem podwojonym, i nieskompresowane strumienie treści muszą być identyczne. Oba biegi lądują na tych samych wartościach punktach po projekcji, więc każda różnica to wartość, która przeskoczyła przeliczenie. Tak zestaw testów HotPDF sprawdza akapity, tabele, import HTML, spłaszczanie XFA, łuki, metafile i obrazy. Ta sama technika działa dla twojego własnego kodu raportów przy niemal zerowym osprzęcie:

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // czytelne strumienie treści
    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)
// muszą produkować identyczne bajt w bajt strumienie treści stron
Jak zweryfikować niezależność od Resolution w kodzie HotPDF Delphi: wyrenderuj identyczny layout dwa razy, raz przy Resolution 72 ze skalą 1 i raz przy 144 z każdą współrzędną i rozmiarem fontu podwojonym, po czym wymagaj identycznych bajt w bajt nieskompresowanych strumieni treści — niezgodność wskazuje stronę przełączoną na UserDefined przez Width albo nieprzeliczoną wartość punktową docierającą do SetFont
Oba biegi lądują na tych samych wartościach punktach po projekcji, więc każda różnica to liczba, która przeskoczyła swoje przeliczenie — ten sam osprzęt, na którym opiera się zestaw testów HotPDF

Sprawdź operatory niosące liczby: Td, Tm, Tf, re, w i tablice TJ. Bajty na poziomie pliku nadal będą się różnić datą utworzenia i /ID, więc porównuj strumienie, a nie całe pliki. Niezgodność niemal zawsze wskazuje jedną z dwóch pułapek z góry: stronę przeskalowaną przez Width albo wartość punktową podaną prosto do SetFont. Jeśli jesteś świeży w samych wywołaniach rysowania, zacznij od przewodnika HotPDF TextOut po rozmiarze, stylu i obrocie, a potem wróć i przełącz Resolution, gdy twój layout czyta UserWidth. Pełne szczegóły API i pobrania próbne są na stronie HotPDF Delphi PDF component