Teknisk artikel

RtLTextOut i HotPDF: Höger-till-vänster PDF-text i Delphi

Skicka den arabiska meningen يوضح ملف PDF هذا till vanlig TextOut och sidan som kommer tillbaka är fel på två sätt samtidigt. Orden löper från vänster till höger istället för från höger till vänster, och bokstäverna sitter isär i sina isolerade former istället för att sammanfogas till sammanhängande ord. Inga felmeddelanden visas. Delphikoden kompileras, filen öppnas, och en granskare som läser arabiska berättar att utdata är oanvändbar. Lösningen är ett anrop, inte att byta bibliotek: HotPDF dirigerar höger-till-vänster-text genom en separat metod, RtLTextOut, som hanterar den omordning vanlig TextOut inte gör. Denna sida är arbetsreferensen för den metoden: signaturen och dess parametrar, teckenuppsättningsargumentet (charset) som väljer skriptet, sidoeffekten på dokumentnivå, typsnittsuppsättningen som måste komma först, och de fel som faktiskt når supporten, var och en med sin lösning

Signatur och parametrar

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

X och Y förankrar körningen i sidans eget koordinatsystem, mätt från det nedre vänstra hörnet med Y växande uppåt, samma origo som varje TextOut-anrop använder; RtLTextOut ändrar glyfordningen, inte var sidan mäter från. angle roterar baslinjen exakt som den gör i TextOut, så 0 ritar en horisontell linje. Text är strängen i logisk ordning, den ordning du skulle skriva den i, och den andra överlagringen tar samma UTF-16-data som en rå PWORD-buffert med ett uttryckligt antal kodenheter, vilket är formen att använda när texten anländer från ett API snarare än en Delphi-sträng. På äldre Delphi-versioner som föregår överlagringsupplösning för dessa typer exponeras strängformen under namnet RtLTextOutStr med en identisk parameterlista

Arbetsfördelningen mellan de två utdataanropen är strikt. TextOut ritar kodpunkter i den ordning du skickar dem, vilket är korrekt för latinska, kyrilliska och CJK-tecken och fel för arabiska och hebreiska. RtLTextOut ordnar om varje rad i visuell höger-till-vänster-ordning först, ritar sedan, och håller inbäddade latinska ord och siffror läsande från vänster till höger inuti raden. HotPDF håller de två metoderna avsiktligt separata snarare än att gissa riktning från tecknen, så valet av vilken man ska anropa är valet av vilket skriptbeteende du får; använd RtLTextOut för höger-till-vänster-körningar, TextOut för allt annat, och dirigera aldrig den ena genom den andra. Varför omordningen överhuvudtaget existerar, vad Unicode Bidirectional Algorithm och arabisk kontextuell sammanfogning faktiskt gör, och var HotPDF:s formning stannar är ämnet för det medföljande inlägget om Arabisk och RTL-textformning med HotPDF; allt nedan är den praktiska uppsättningen

Diagram över hur RtLTextOut ordnar om en blandad arabisk och latinsk rad till visuell höger-till-vänster-ordning innan den ritas in i en PDF
RtLTextOut ordnar om varje rad till visuell ordning innan den ritas: höger-till-vänster-körningar behåller sin sekvens medan inbäddade latinska ord och siffror läses från vänster till höger inuti raden.

Teckenuppsättningsargumentet (charset) bestämmer skriptet

Vad som berättar för RtLTextOut om den layoutar arabiska eller hebreiska är inte metoden, det är typsnittet. SetFont tar en Windows-teckenuppsättning (charset) som sitt fjärde argument, och det värdet bär skriptreglerna in i höger-till-vänster-anropet: 178 väljer arabiska, 177 väljer hebreiska. Ställ in teckenuppsättningen, rita sedan, så kommer de två raderna nedan ut i korrekt läsordning utan ytterligare konfiguration

// Arabic: charset 178 tells RtLTextOut to apply Arabic rules
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebrew: charset 177 switches the rules to Hebrew
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

En sekvenseringsdetalj är lätt att missa: SetFont måste komma först och måste upprepas efter varje AddPage, eftersom det aktuella typsnittet, inklusive teckenuppsättning, inte överlever en sidbrytning. Glömmer du upprepningen faller den andra sidan tillbaka på vilket typsnitt som än var aktivt, vilket för arabiska vanligtvis innebär tomma rutor

Den vänder inte text som du redan har vänt på

Det enda misstag som slukar mest felsökningstid här är att mata RtLTextOut med en sträng som du redan vänt för hand. Folk når denna metod efter att ett första försök med vanlig TextOut kom ut baklänges, och en vanlig tillfällig lösning är att vända tecknen i koden innan man ritar. RtLTextOut vänder internt på egen hand, så en förvänd sträng blir vänd en andra gång och landar rakt tillbaka där den startade. Skicka texten i logisk ordning, den ordning du skulle skriva den i och läsa den högt, och låt anropet göra omordningen

Fällan är obehagligare än en vanlig vändning eftersom en dubbelvänd sträng kan se korrekt ut för en testfras som är helt på arabiska, och sedan gå sönder i samma ögonblick som en rad innehåller ett latinskt ord eller en siffra. Inuti en höger-till-vänster-rad förväntas dessa inbäddade körningar läsas från vänster till höger, och handvändning förstör den nästlingen medan det rent arabiska fallet råkar överleva den. Så buggen seglar igenom ditt första röktst och dyker upp senare på en riktig faktura med ett kontonummer i den. Plocka bort varje manuell vändning i samma ögonblick som du byter till RtLTextOut

Sidoeffekten på Riktning (Direction) som är värd att veta

Att anropa RtLTextOut ändrar mer än raden du ritar. Det vänder också dokumentets preferens för läsriktning till höger-till-vänster, samma sak som du annars skulle ställa in själv genom Direction-egenskapen. Den inställaren lägger till vpDirection i dokumentets ViewerPreferences, vilket talar om för ett visningsprogram hur två-upp-uppslag ska ordnas och vilken sida en layout med motstående sidor ska börja från. När hela dokumentet är på arabiska eller hebreiska är detta exakt vad du vill ha, och du får det gratis

Det är värt att känna till just för att det är osynligt på en enskild sida. Om dokumentet mestadels är vänster-till-höger med ett höger-till-vänster-block, kommer det första RtLTextOut-anropet fortfarande att tippa hela filens preferens, och inget i ditt ensidiga korrektur kommer att visa det. Symptomet uppträder veckor senare när någon skriver ut en dubbelsidig broschyr och uppslagen kommer ut spegelvända. Om det inte är det du vill, ställ explicit tillbaka Direction efter höger-till-vänster-körningen:

// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;

För ett dokument som verkligen läses från höger till vänster, lämna det ifred. Poängen är att veta att anropet har en dokumentomfattande effekt så att broschyröverraskningen aldrig inträffar

Registrera det typsnitt du levererar, inte det du hoppas är installerat

Ingen av omordningarna spelar någon roll om typsnittet inte har några glyfer att rita. Det klassiska misslyckandet är en rapport som renderas felfritt på utvecklarens maskin, där Arial Unicode MS råkar finnas, och kommer ut som rader av tomma rutor på en kunds server där Windows i tysthet bytte ut till ett typsnitt som saknar täckning för arabiska överhuvudtaget. Botemedlet är att sluta lita på installerade systemtypsnitt och registrera ett som du levererar med applikationen

// Ship a known Arabic font and register it before drawing
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Två gränser åker med vid registrering. Ett typsnitt som hämtas in via RegisterUnicodeTTF blir inbäddat, och HotPDF:s hantering av inbäddad Unicode kräver att dokumentet är på PDF 1.5 eller senare; det biter bara ifall något längre ner i kedjan insisterar på PDF 1.4, men när det händer är misslyckandet tyst. Det andra är juridiskt snarare än tekniskt: TrueType-filer bär på inbäddningstillståndsbitar, och ett utseende som ser bra ut på skärmen kan vara licensierat på ett sätt som förbjuder att det levereras inuti kunddokument. Bekräfta licensen innan du bäddar in, inte efter ett klagomål

Ett komplett konsolexempel

Genom att sätta ihop bitarna är här ett fristående program som skriver en sida med en arabisk rad, en hebreisk rad, och en blandad rad som bär ett latinskt produktnamn. Varje block ställer in sin teckenuppsättning, sedan ritar den i logisk ordning

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // HotPDF main unit

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'RtLTextOut.pdf';
    Pdf.BeginDoc;

    // A Latin heading goes through the ordinary TextOut path
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Arabic: charset 178, logical order, RtLTextOut does the reordering
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // Hebrew: charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // Mixed line: the embedded Latin word still reads left to right
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 640, 0,
      'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');

    Pdf.EndDoc;
    Writeln('Wrote RtLTextOut.pdf');
  finally
    Pdf.Free;
  end;
end.

Kör det och öppna resultatet. De arabiska och hebreiska raderna läses från höger till vänster, bokstäverna fogas samman där skriptet fogar samman dem, och på den sista raden sitter namnet HotPDF vänster-till-höger inuti den arabiska körningen. Den nästlingen är det korrekta dubbelriktade resultatet, inte en bugg, även om förstagångsgranskare rutinmässigt anmäler det som en; artikeln om formning (shaping) länkad ovan förklarar varför Unicode-reglerna kräver det och hur man formulerar sina acceptanskriterier så att felrapporten aldrig lämnas in

Vanliga fel och deras lösningar

Varje felnedan har förekommit i en verklig supporttråd, och var och en kan spåras tillbaka till en av sektionerna ovan

  • Utdata läses baklänges eller rörs ihop på blandade rader — strängen vändes för hand före anropet, oftast en kvarbliven nödlösning från ett TextOut-försök. Radera varje manuell vändning och skicka logisk ordning; RtLTextOut vänder internt
  • Bokstäver skrivs ut bortkopplade i isolerade former — texten gick genom vanlig TextOut, eller SetFont anropades utan en teckenuppsättning för höger-till-vänster. Rita med RtLTextOut och skicka 178 för arabiska eller 177 för hebreiska som det fjärde SetFont-argumentet
  • Tomma rutor på kundens maskin — Windows bytte ut till ett typsnitt utan arabisk eller hebreisk täckning. Sluta namnge installerade typsnitt; registrera ett utseende som du levererar via RegisterUnicodeTTF och använd SetFont med det namnet
  • Andra sidan renderas i fel typsnitt — det aktuella typsnittet överlever inte AddPage. Upprepa SetFont-anropet, teckenuppsättning inkluderad, efter varje sidbrytning
  • Dubbelsidiga uppslag skrivs ut spegelvända på ett mestadels LTR-dokument — det första RtLTextOut-anropet vände på dokumentets Direction som en sidoeffekt. Sätt Pdf.Direction := LeftToRight efter höger-till-vänster-körningen
  • Inbäddad Unicode-text försämras tyst nedströms — något i kedjan tvingar PDF 1.4, och HotPDF:s hantering av inbäddad Unicode behöver 1.5 eller senare. Höj dokumentversionen eller ta bort nedströmsbegränsningen

Innan formatet levereras, verifiera förbi enbart en titt: kopiera ut texten från visningsprogrammet igen, kör in-dokument-sökningen, öppna filen på en maskin utan dina utvecklingsfonter, och lägg ett genuint dokument framför en modersmålstalande läsare. Den fullständiga verifieringschecklistan, täckningskartan per skript, och teststrängskorpusen som är värd att bygga lever alla i det medföljande inlägget om Arabisk och RTL-textformning med HotPDF

De anrop av RtLTextOut, SetFont och RegisterUnicodeTTF som visas här är en del av HotPDF-komponenten för Delphi och C++Builder