Teknisk artikel

RtLTextOut i HotPDF: Højre-til-venstre PDF-tekst i Delphi

Send den arabiske sætning يوضح ملف PDF هذا til almindelig TextOut, og den side, der kommer tilbage, er forkert på to måder på én gang. Ordene løber fra venstre mod højre i stedet for højre mod venstre, og bogstaverne sidder adskilt i deres isolerede former i stedet for at samles til forbundne ord. Ingenting fejler. Delphi'en kompilerer, filen åbnes, og en anmelder, der læser arabisk, fortæller dig, at outputtet er ubrugeligt. Løsningen er ét kald, ikke en udskiftning af bibliotek (library swap): HotPDF dirigerer højre-til-venstre-tekst gennem en særskilt metode, RtLTextOut, der håndterer den omordning (reordering), som almindelig TextOut ikke vil. Denne side er arbejdsreferencen for den metode: signaturen og dens parametre, charset-argumentet (tegnsæt), der vælger scriptet, bivirkningen på dokumentniveau, skrifttypeopsætningen, der skal komme først, og de fejl, der faktisk når support, hver med sin rettelse

Signatur og parametre

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

X og Y forankrer forløbet i sidens eget koordinatsystem, målt fra det nederste venstre hjørne med Y voksende opad, samme origo som ethvert TextOut-kald bruger; RtLTextOut ændrer glyfrækkefølgen, ikke hvor siden måler fra. angle roterer grundlinjen (baseline) nøjagtigt som den gør i TextOut, så 0 tegner en horisontal linje. Text er strengen i logisk rækkefølge, den rækkefølge du ville skrive den, og den anden overload tager de samme UTF-16 data som en rå PWORD-buffer med et eksplicit code-unit-antal, hvilket er den form, der skal bruges, når teksten ankommer fra en API snarere end en Delphi-streng. På ældre Delphi-versioner, der går forud for overload-opløsning for disse typer, eksponeres strengformen under navnet RtLTextOutStr med den identiske parameterliste

Arbejdsdelingen mellem de to output-kald er streng. TextOut tegner kodepunkter i den rækkefølge, du overfører dem, hvilket er korrekt for latinsk, kyrillisk og CJK og forkert for arabisk og hebraisk. RtLTextOut omordner hver linje til visuel højre-til-venstre-rækkefølge først, og tegner derefter, idet indlejrede latinske ord og cifre fortsat læses fra venstre til højre inde i linjen. HotPDF holder de to metoder bevidst adskilt i stedet for at gætte retningen ud fra tegnene, så valget af hvilken man kalder, er valget af hvilken scriptadfærd man får; brug RtLTextOut til højre-til-venstre-forløb, TextOut til alt andet, og før aldrig den ene gennem den anden. Hvorfor omordningen overhovedet eksisterer, hvad Unicodes tovejs-algoritme (Bidirectional Algorithm) og arabisk kontekstuel sammensætning (contextual joining) rent faktisk gør, og hvor HotPDF's formning (shaping) stopper, er emnet for ledsageartiklen om arabisk og RTL tekstformning med HotPDF; alt nedenfor er den praktiske opsætning

Diagram over hvordan RtLTextOut omordner en blandet arabisk og latinsk linje til visuel højre-til-venstre-rækkefølge før tegning i en PDF
RtLTextOut omordner hver linje til visuel rækkefølge før tegning: højre-til-venstre-forløb beholder deres sekvens, mens indlejrede latinske ord og cifre læses fra venstre mod højre inde i linjen.

Charset-argumentet beslutter scriptet

Det, der fortæller RtLTextOut, om det lægger arabisk eller hebraisk ud, er ikke metoden, det er skrifttypen. SetFont tager et Windows-tegnsæt (charset) som dets fjerde argument, og den værdi bærer scriptreglerne ind i højre-til-venstre-kaldet: 178 vælger arabisk, 177 vælger hebraisk. Indstil tegnsættet, tegn derefter, og de to linjer nedenfor kommer ud i korrekt læserækkefølge uden yderligere 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 זה');

Én rækkefølgedetalje er let at overse: SetFont skal komme først og skal gentages efter hver AddPage, fordi den aktuelle skrifttype, inklusiv tegnsæt, ikke overlever et sideskift. Glem gentagelsen, og den anden side falder tilbage til, hvilken skrifttype der end var aktiv, hvilket for arabisk normalt betyder tomme bokse

Det vender ikke tekst, du allerede har vendt

Den ene fejl, der opsluger mest fejlfindingstid her, er at fodre RtLTextOut med en streng, du allerede har vendt manuelt. Folk når frem til denne metode, efter et første forsøg med almindelig TextOut kom baglæns ud, og en almindelig nødløsning er at vende tegnene i koden før tegning. RtLTextOut vender internt på egen hånd, så en præ-vendt streng bliver vendt en anden gang og lander lige tilbage, hvor den startede. Videregiv teksten i logisk rækkefølge, den rækkefølge du ville skrive den og læse den højt, og lad kaldet foretage omordningen

Fælden er styggere end en simpel vending, fordi en dobbelt-vendt streng kan se korrekt ud for én rent-arabisk testfrase og derefter gå i stykker i det øjeblik, en linje bærer et latinsk ord eller et tal. Inde i en højre-til-venstre-linje formodes de indlejrede forløb at læse fra venstre til højre, og manuel vending ødelægger den indlejring (nesting), mens den rene arabiske case tilfældigvis overlever det. Så fejlen sejler gennem din første røgstest (smoke test) og dukker op senere på en rigtig faktura med et kontonummer i. Fjern enhver manuel vending i det øjeblik, du skifter til RtLTextOut

Direction-bivirkningen der er værd at kende

At kalde RtLTextOut ændrer mere end den linje, du tegner. Det vender også dokumentets læseretnings-præference til højre-til-venstre, det samme du ellers ville sætte selv gennem Direction-egenskaben (property). Den setter (setter) tilføjer vpDirection til dokumentets ViewerPreferences, hvilket fortæller en fremviser (viewer), hvordan man arrangerer to-op (two-up) opslag, og hvilken side et opslag (facing-page layout) starter fra. Når hele dokumentet er arabisk eller hebraisk, er dette præcis, hvad du vil have, og du får det gratis

Det er værd at kende til netop, fordi det er usynligt på en enkelt side. Hvis dokumentet overvejende er venstre-til-højre med én højre-til-venstre-blok, vil det første RtLTextOut-kald stadig tippe hele filens præference, og intet i din énsides-prøve (proof) vil vise det. Symptomet vises uger senere, når nogen udskriver en dupleks-brochure (duplex booklet), og opslagene kommer spejlvendte ud. Hvis det ikke er det, du vil have, så sæt Direction tilbage eksplicit efter højre-til-venstre-forløbet:

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

For et dokument, der virkelig læses fra højre til venstre, lad det være. Pointen er at vide, at kaldet har en dokumentdækkende effekt, så brochure-overraskelsen aldrig sker

Registrer den skrifttype, du sender (ship), ikke den, du håber er installeret

Intet af omordningen betyder noget, hvis skrifttypen ikke har nogen glyffer at tegne. Den klassiske fejl er en rapport, der renderes fejlfrit på udviklerens maskine, hvor Arial Unicode MS tilfældigvis er til stede, og kommer ud som rækker af tomme bokse på en kundes server, hvor Windows stille erstattede en skrifttype uden nogen arabisk dækning overhovedet. Kuren er at stoppe med at stole på installerede systemskrifttyper og registrere én, du sender 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 هذا');

To grænser følger med registrering. En skrifttype indbragt gennem RegisterUnicodeTTF bliver indlejret (embedded), og HotPDF's indlejrede Unicode-håndtering har brug for dokumentet ved PDF 1.5 eller nyere; det bider kun, hvis noget downstream insisterer på PDF 1.4, men når det gør, er fejlen lydløs. Den anden er juridisk snarere end teknisk: TrueType-filer bærer tilladelsesbits til indlejring (embedding-permission bits), og et skrifttype-face, der ser fint ud på skærmen, kan være licenseret på en måde, der forbyder at sende den inde i kundedokumenter. Bekræft licensen, før du indlejrer, ikke efter en klage

Et komplet konsol-eksempel

Ved at sætte brikkerne sammen, er her et selvstændigt program (self-contained program), der skriver én side med en arabisk linje, en hebraisk linje og en blandet linje, der bærer et latinsk produktnavn. Hver blok indstiller sit tegnsæt og tegner derefter i logisk rækkefølge

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 og åbn resultatet. De arabiske og hebraiske linjer læses fra højre til venstre, bogstaverne samles, hvor scriptet samler dem, og i den sidste linje sidder tokenet HotPDF fra venstre til højre inde i det arabiske forløb. Den indlejring er det korrekte tovejsresultat (bidirectional result), ikke en fejl, selvom førstegangs-anmeldere (reviewers) rutinemæssigt anmelder det som én; formningsartiklen linket ovenfor forklarer, hvorfor Unicode-reglerne kræver det, og hvordan man formulerer sine acceptkriterier, så rapporten aldrig bliver indgivet

Almindelige fejl og deres rettelser

Enhver fejl nedenfor har optrådt i en rigtig supporttråd, og hver sporer tilbage til en af sektionerne ovenfor

  • Output læses baglæns eller blandes på blandede linjer — strengen blev vendt manuelt før kaldet, normalt en efterladt nødløsning fra et TextOut-forsøg. Slet enhver manuel vending og videregiv logisk rækkefølge; RtLTextOut vender internt
  • Bogstaver udskrives afbrudt i isolerede former — teksten gik gennem almindelig TextOut, eller SetFont blev kaldt uden et højre-til-venstre tegnsæt. Tegn med RtLTextOut og giv 178 for arabisk eller 177 for hebraisk som det fjerde SetFont-argument
  • Tomme bokse på kundens maskine — Windows erstattede en skrifttype uden nogen arabisk eller hebraisk dækning. Stop med at navngive installerede skrifttyper; registrer et face (skrifttype), du sender (ship), gennem RegisterUnicodeTTF og anvend SetFont med det navn
  • Anden side renderes i den forkerte skrifttype — den aktuelle skrifttype overlever ikke AddPage. Gentag SetFont-kaldet, inklusive tegnsæt, efter hvert sideskift
  • Dupleks-opslag udskrives spejlvendt på et overvejende LTR-dokument — det første RtLTextOut-kald vendte dokumentets Direction (retning) som en bivirkning. Sæt Pdf.Direction := LeftToRight efter højre-til-venstre-forløbet
  • Indlejret Unicode-tekst forringes (degrades) stille downstream — noget i pipelinen gennemtvinger PDF 1.4, og HotPDF's indlejrede Unicode-håndtering behøver 1.5 eller nyere. Hæv dokumentversionen eller fjern downstream-begrænsningen

Før formatet udgives (ships), skal du verificere ud over blot at vurdere med øjet: kopier teksten tilbage ud af fremviseren, kør søgningen i dokumentet, åbn filen på en maskine uden dine udviklingsskrifttyper, og læg ét ægte dokument foran en indfødt læser. Den fulde verificeringscheckliste, dækningskortet pr. script og teststreng-korpusset (test-string corpus), der er værd at bygge, lever alle i ledsageartiklen om arabisk og RTL tekstformning med HotPDF

RtLTextOut-, SetFont- og RegisterUnicodeTTF-kaldene, der er vist her, er en del af HotPDF-komponenten til Delphi og C++Builder