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
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
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
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
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
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