Artykuł techniczny

Funkcja TextOut w HotPDF (Delphi)

Ciąg narysowany na (72, 72) ląduje cal poniżej górnej krawędzi strony, co łapie każdego, kto uczył się PDF-a od strony strumienia treści, gdzie y = 72 siedzi cal nad spodem. Druga niespodzianka wychodzi kilkaset linijek dalej. Raport rozłożony na US Letter wraca z pierwszą stroną 612 na 792 punkty i każdą następną 595 na 842, bo linijka ustawiająca rozmiar papieru wykonała się dokładnie raz

Oba efekty mają ten sam korzeń. Tekst w PDF-ie to krótki pas zmian stanu (czcionka i rozmiar, odstęp znaków, odstęp słów, macierz tekstu dla pozycji i obrotu) zamknięty jednym operatorem pokazania tekstu, a wysokopoziomowe wywołanie tekstowe jest cienkim wrapperem nad tą sekwencją. Wywołanie czyta stan. Nigdy nie zeruje go za ciebie, a nowa strona startuje ze stałego zestawu domyślnych, a nie z tego, na czym skończyła poprzednia

Gdzie TextOut kładzie (X, Y) na stronie

HotPDF rysuje tekst przez THPDFPage.TextOut(X, Y: Single; angle: Extended; Text: WideString), osiągane jako Pdf.CurrentPage.TextOut. X i Y mierzą się od lewego górnego rogu strony, z Y rosnącym w dół, a przy domyślnym Resolution równym 72 jedna jednostka to jeden punkt, 1/72 cala. Strona przelicza każde Y na rosnącą od dołu przestrzeń użytkownika PDF, odejmując je od wysokości strony. Kąt jest w stopniach, a ponieważ tekst to WideString, Unicode przechodzi prosto. Druga przeciążona wersja przyjmuje PWORD plus długość, dla kodu, który już trzyma bufory UTF-16

Geometria strony też jest ustalona. BeginDoc dodaje pierwszą stronę, a każda strona stworzona przez BeginDoc albo AddPage startuje jako A4, 595 na 842 punkty, z Arial 12 jako bieżącą czcionką. US Letter (612 na 792) dzieli jedno przypisanie przez CurrentPage.Size := Letter, ale to przypisanie należy do strony, więc trzeba je powtarzać dla każdej nowej

Domyślnie Y, który podajesz, to góra tekstu, a linia bazowa siedzi o jedno wznoszenie czcionki niżej. TextFromBaseLine(True) przełącza stronę na pozycjonowanie po linii bazowej. Włącz je dla wszystkiego, co miesza rozmiary na jednej linii. W trybie górnej krawędzi kod waluty 9-punktowy obok kwoty 14-punktowej ma górę zrównaną z górą kwoty, więc wyraźnie podskakuje; w trybie linii bazowej oba ciągi siedzą na tej samej linii, tak jak ustawiłby je zecer. Ostatni blok następnego przykładu robi dokładnie to

Rozmiar i styl mieszkają w SetFont

TextOut nie ma parametru rozmiaru ani stylu. SetFont(FontName, FontStyle, ASize, FontCharset, IsVertical) dźwiga to wszystko, dwa ostatnie argumenty domyślając się do ANSI_CHARSET i tekstu poziomego, a cokolwiek ustawi, pozostaje w mocy dla każdego TextOut-a, aż do następnego SetFont. Większość zamieszania pierwszego dnia wraca dokładnie do tego. Linia wychodzi pogrubiona, bo trzy wywołania wcześniej coś ustawiło [fsBold] i nic tego nie wyczyściło

var
  W: Single;
begin
  Pdf.BeginDoc;                                     // pierwsza strona: A4, Arial 12
  Pdf.CurrentPage.TextFromBaseLine(True);           // każde Y poniżej to linia bazowa

  Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
  Pdf.CurrentPage.TextOut(72, 72, 0, 'Quarterly Report');        // 24 pt zwykła

  Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
  Pdf.CurrentPage.TextOut(72, 104, 0, 'Revenue');                // 12 pt pogrubiona

  Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
  Pdf.CurrentPage.TextOut(72, 120, 0, 'figures in thousands');   // 11 pt kursywa

  Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsUnderline], 10);
  Pdf.CurrentPage.TextOut(72, 136, 0, '+18.4% YoY');             // style się łączą

  // Różne rozmiary na jednej linii bazowej: mierz, póki bieżący jest krój 14 pt
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
  W := Pdf.CurrentPage.GetWideTextWidth('1,284.50');
  Pdf.CurrentPage.TextOut(72, 160, 0, '1,284.50');
  Pdf.CurrentPage.SetFont('Arial', [], 9);
  Pdf.CurrentPage.TextOut(72 + W + 3, 160, 0, 'EUR');            // odstęp 3 pt
  ...
end;

Argument stylu to zwykły zbiór TFontStyles, więc [] to tekst zwykły, a [fsBold, fsItalic] to pogrubiona kursywa. fsUnderline i fsStrikeOut rysowane są jako cienkie wypełnione paski pod ciągiem albo przez niego i podążają za ciągiem, gdy jest obracany. Rozmiar jest w punktach, tej samej jednostce co współrzędne, co trzyma pionową arytmetykę prostą: tekst treści 11 punktów z krokiem 16 punktów zostawia około pięć punktów powietrza między liniami. Nie ma automatycznego przejścia do nowej linii. Każde Y obliczasz sam, co jest żmudne dla akapitu i dokładnie właściwe dla formularza, którego pola siedzą na stałych pozycjach

GetWideTextWidth mierzy czcionką bieżącą w chwili wywołania, więc w ostatnim bloku pomiar musi się zdarzyć, póki pogrubiony krój 14 punktów jest jeszcze ustawiony. Przesuń go pod drugie SetFont, a 9-punktowy przyrostek wyląduje na ostatnich cyfrach kwoty

Nazwa czcionki rozwiązuje się względem czcionek zainstalowanych na maszynie budującej PDF, a krój, który rozwiązuje się na biurku programisty, może być nieobecny na serwerze budującym, więc krój w skończonym pliku zależy od maszyny, która go wyprodukowała. Pokrycie skryptów to druga połowa. Krój wyłącznie łaciński poproszony o cyrylicę albo CJK produkuje prostokąty brakujących glifów, i to jest własność pliku czcionki, nie wywołania. Gdy jeden ciąg miesza skrypty, których żaden pojedynczy krój nie pokrywa, TextOutWithFallback przyjmuje te same cztery argumenty, przełącza kroje ciąg po ciągu dla znaków, których brakuje czcionce głównej, i przywraca pierwotną czcionkę po wszystkim

Strona z przykładowym tekstem HotPDF TextOut pokazująca czcionki Arial, Times New Roman i Courier New w stylach normalnym, pogrubionym i kursywie przy użyciu różnych zestawów znaków

Jak argument kąta obraca tekst?

Niezerowy kąt obraca ciąg przeciwnie do ruchu wskazówek zegara o tyle stopni wokół jego kotwicy, a kotwica to lewy górny róg tekstu w jego własnym układzie. Kotwica się nie rusza. Zmienia się tylko kierunek, w którym maszerują glify, więc współrzędna, która umieszczała poziomą etykietę, umieszcza też jej obrócony bliźniak. Obrócone ciągi trzymają tę kotwicę lewego górnego rogu nawet, gdy strona jest w trybie linii bazowej; TextFromBaseLine przesuwa wyłącznie ciągi poziome

Diagram geometrii obrotu TextOut w HotPDF: przebiegi pod 0, 45 i 90 stopni obracające się przeciwnie do wskazówek zegara wokół stałej kotwicy, z przykładami wywołań Delphi TextOut dla etykiet grzbietów, znaków wodnych i pochylonych nagłówków kolumn
Niezerowy kąt obraca ciąg przeciwnie do ruchu wskazówek zegara wokół nieruchomej kotwicy, czyli lewego górnego rogu tekstu, a wspólne linie bazowe krokują X dokładnie tak, jak poziome wiersze krokują Y
var
  W, AxisMidY: Single;
begin
  AxisMidY := 400;                                // środek osi Y wykresu

  // Ciąg pod kątem 90 stopni czyta się od dołu do góry i rośnie w górę od Y,
  // więc wyśrodkowanie go na osi to start o pół szerokości poniżej środka
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);
  W := Pdf.CurrentPage.GetWideTextWidth('Units sold');
  Pdf.CurrentPage.TextOut(40, AxisMidY + W / 2, 90, 'Units sold');

  // Ukośny znak wodny
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
  Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');

  // Nagłówki kolumn przechylone o 60 stopni: to samo Y, krok X
  Pdf.CurrentPage.SetFont('Arial', [], 9);
  Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
  Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');
end;

Dziewięćdziesiąt stopni pokrywa etykiety osi i tytuły grzbietów. Przy 90 stopniach glify stoją na prawo od X kotwicy, szerokie na jedną wysokość linii, a ciąg wspina się ku górze strony własną szerokością. Dlatego wyśrodkowanie dodaje połowę szerokości do Y zamiast odejmować, i dlatego etykieta zakotwiczona w samym środku siedzi całkowicie nad nim. Sześćdziesiąt stopni pasuje do nagłówków kolumn, pozwalając długiej etykiecie stanąć nad wąską kolumną bez wylewania się na sąsiada, a 45 stopni to zwykły kąt znaku wodnego. Ciągi dzielące linię bazową dostają to samo Y i krokujące X, dokładnie tak, jak ułożone jedna nad drugą linie poziome dzielą X i krokują Y

Dlaczego druga strona wychodzi jako A4 w Arial 12?

AddPage zwraca indeks nowej strony i robi ją bieżącą, a nowa strona startuje z domyślnych: A4, Arial 12, pozycjonowanie po górnej krawędzi, zerowe odstępy znaków i słów. Nic z poprzedniej strony nie przechodzi. Ten projekt trzyma każdą stronę samowystarczalną, bo strumień treści strony PDF i tak startuje ze świeżym stanem graficznym, ale znaczy też, że każdy kod układu konfigurujący stronę raz na szczycie dokumentu po cichu przełączy rozmiar papieru i krój na stronie drugiej. Naprawa polega na tym, by wszystkie ustawienia per strona zamknąć w jednej procedurze i wołać ją po BeginDoc i po każdym AddPage

HotPDF: Diagram strony US Letter pokazujący rachunek pozycji linii z lewym marginesem, górną linią startową i stałym Leading kroczącym ku limitowi dolnego marginesu, obok pętli układu z nazwanymi stałymi z ręczną strażą AddPage i resetem SetFont
Nazwane kotwice zamieniają kolumnę magicznych liczb w arytmetykę: zwiększaj bieżące Y o Leading w każdym wierszu i sam doglądaj dolnego marginesu przez AddPage
procedure StartReportPage(Pdf: THotPDF);
begin
  Pdf.CurrentPage.Size := Letter;               // nowa strona startuje jako A4
  Pdf.CurrentPage.TextFromBaseLine(True);       // nowa strona startuje w trybie górnej krawędzi
  Pdf.CurrentPage.SetFont('Arial', [], 11);     // nowa strona startuje w Arial 12
end;

procedure WriteReport(Pdf: THotPDF; Lines: TStrings);
const
  LeftMargin   = 72;   // 1 cal
  TopLine      = 72;   // pierwsza linia bazowa, cal w dół
  BottomMargin = 72;
  Leading      = 16;
var
  I: Integer;
  Y: Single;
begin
  StartReportPage(Pdf);
  Y := TopLine;
  for I := 0 to Lines.Count - 1 do
  begin
    if Y > Pdf.CurrentPage.Height - BottomMargin then   // 720 na Letter
    begin
      Pdf.AddPage;
      StartReportPage(Pdf);
      Y := TopLine;
    end;
    Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Lines[I]);
    Y := Y + Leading;
  end;
end;

Pod TextOut-em nie ma żadnego układu przepływowego, a Y za dolną krawędzią strony jest wciąż poprawną współrzędną, o ile chodzi o strumień treści: tekst jest zapisany, po prostu wypada poza media box i żaden podgląd go nie pokaże. Dlatego pętla sprawdza Y przed każdą linią i sama otwiera nową stronę. Czytanie limitu z CurrentPage.Height zamiast na sztywno wpisanego 720 trzyma pętlę poprawną, gdy ktoś później przełączy helper z powrotem na A4, gdzie ten sam calowy margines kładzie limit na 770

Najszybszy sprawdzian to ten, który zrobiłby dialog drukowania podglądu. Otwórz wynik, przejdź na stronę drugą i spójrz na właściwości dokumentu: strona Letter raportuje 8,5 na 11 cali, strona A4 około 8,27 na 11,69. Jeśli pierwsza strona i druga się różnią, jakieś ustawienie per strona mieszka poza helperem

Odstępy znaków i słów to też stan strony

Czasem ciąg jest właściwy, a szerokość zła: nagłówek, który ma rozpiąć się na stałą linijkę, kod, który czyta się lepiej z luźniejszymi cyframi, linia etykiet, której przerwy trzeba rozchylić. PDF ma na to dwa parametry stanu tekstu w ISO 32000-1: odstęp znaków Tc (§9.3.2), dochodzący po każdym glifie, i odstęp słów Tw (§9.3.3), dochodzący przy każdej spacji. HotPDF ustawia je przez SetCharacterSpacing(Spacing: Single) i SetWordSpacing(Spacing: Single), w punktach przy domyślnej rozdzielczości, i jak czcionka pozostają w mocy, aż ktoś je zmieni

HotPDF: Rozstrzelenie znaków Tc rozdzielające dodatkowy luz równo po każdym glifie SUMMARY kontra rozstrzelenie słów Tw gromadzące go tylko przy spacjach, wpięte w cykl ustaw, rysuj, reset, który trzyma stan rozstrzelenia z dala od przecieku do późniejszych akapitów
Tc rozprowadza swoją korektę na każdy glif, podczas gdy Tw działa tylko na znak spacji, a resetowanie w następnym wierszu utrzymuje stan rysowania w czystości

Odstęp słów ma haczyk zapisany w samej specyfikacji. §9.3.3 stosuje Tw wyłącznie do jednobajtowego kodu znaku 32 i wyraźnie nie do bajtu 32 wewnątrz kodu wielobajtowego, więc sam z siebie operator nie robi nic dla tekstu zapisanego dwubajtowymi kodami glifów — a tak normalnie zapisywane są osadzone czcionki Unicode. HotPDF zapisuje ciągi Unicode właśnie tak i na tej ścieżce stosuje odstęp słów jako jawne korekty pozycji po każdym glifie spacji, więc SetWordSpacing rozszerza przerwy i tak, i tak. Wyzwalaczem pozostaje zwykła spacja, U+0020. Ciąg chiński albo japoński bez spacji ASCII nie dostaje żadnego dodatkowego luzu, a spacja ideograficzna (U+3000) traktowana jest jak zwykły glif

Odstęp znaków to właściwe narzędzie, gdy nagłówek musi osiągnąć docelową szerokość, bo rozprowadza zmianę równomiernie zamiast gromadzić ją przy spacjach. GetWideTextWidth liczy odstęp znaków i słów aktualnie w mocy, więc mierz przy obu na zerze, a odstęp wyprowadź potem:

const
  Heading = 'SUMMARY';
  Target  = 200;        // szerokość linijki, którą nagłówek ma rozpiąć, w punktach
var
  Natural, Tc: Single;
begin
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // mierz bez resztkowego odstępu
  Natural := Pdf.CurrentPage.GetWideTextWidth(Heading);

  // Tc dochodzi po każdym glifie, ale widoczne są tylko przerwy między glifami
  Tc := (Target - Natural) / (Length(Heading) - 1);

  Pdf.CurrentPage.SetCharacterSpacing(Tc);      // wartości ujemne ściskają
  Pdf.CurrentPage.TextOut(72, 72, 0, Heading);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // reset przed tekstem treści

  Pdf.CurrentPage.SetWordSpacing(6);
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Pdf.CurrentPage.TextOut(72, 100, 0, 'Name Department Extension');
  Pdf.CurrentPage.SetWordSpacing(0);
end;

Linijki resetu znaczą więcej niż linijki ustawiania. Zostaw odstęp znaków 4 punkty po jednym nagłówku, a każdy akapit pod nim odziedziczy rozciągnięcie, co czyta się jako mgliste coś-nie-tego, które przeżyje pobieżną korektę, po czym wyskoczy w drukowanym egzemplarzu. Ustaw wartość, narysuj jeden ciąg, który jej potrzebuje, i wyzeruj w następnej linijce, żeby żaden późniejszy kod nie musiał wiedzieć, co zrobiła wcześniejsza sekcja. Ta sama strona oferuje SetHorizontalScaling (Tz, domyślnie 100) do ściskania ciągu bez zmiany jego wysokości oraz SetTextRenderingMode z wartościami jak trStroke dla tekstu konturowego — oba trwają dokładnie tak samo

Strona HotPDF TextOut porównująca poziome skalowanie tekstu, odstępy między znakami, odstępy między słowami oraz tryby renderowania wypełnienia i obrysu

Jeśli to twoja pierwsza strona wyniku, przykład HotPDF Hello World prowadzi przez ustawienie dokumentu i zapis. Dla tekstu, który musi wypełnić kolumnę od krawędzi do krawędzi, pełne justowanie tekstu PDF ma własny przewodnik, a CJK biegnący z góry na dół omawia pionowy układ tekstu w HotPDF. TextOut, SetFont, wywołania odstępów i ustawienia rozmiaru strony powyżej przychodzą w komplecie z komponentem HotPDF dla Delphi i C++Buildera