Teknisk artikkel

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

Send den arabiske setningen يوضح ملف PDF هذا til vanlig TextOut, og siden som kommer tilbake er feil på to måter på en gang. Ordene løper fra venstre til høyre i stedet for høyre til venstre, og bokstavene sitter fra hverandre i sine isolerte former i stedet for å føyes sammen til sammenhengende ord. Ingenting feiler. Delphi kompilerer, filen åpnes, og en anmelder som leser arabisk forteller deg at utdataene er ubrukelige. Løsningen er ett kall, ikke et bibliotekbytte: HotPDF ruter høyre-til-venstre tekst gjennom en egen metode, RtLTextOut, som håndterer omorganiseringen (reordering) som vanlig TextOut ikke vil. Denne siden er den fungerende referansen for den metoden: signaturen og dens parametere, charset-argumentet som velger skriptet (script), bivirkningen på dokumentnivå, font-oppsettet som må komme først, og feilene som faktisk når support, hver med sin løsning

Signatur og parametere

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 kjøringen (run) i sidens eget koordinatsystem, målt fra nederste venstre hjørne med Y økende oppover, det samme utgangspunktet hvert TextOut-kall bruker; RtLTextOut endrer glyff-rekkefølgen, ikke hvor siden måler fra. angle roterer grunnlinjen (baseline) nøyaktig slik det gjøres i TextOut, så 0 tegner en horisontal linje. Text er strengen i logisk rekkefølge, rekkefølgen du ville ha skrevet den, og den andre overbelastningen (overload) tar de samme UTF-16-dataene som en rå PWORD-buffer med en eksplisitt kodeenhetsteller (code-unit count), som er formen du skal bruke når teksten ankommer fra et API snarere enn en Delphi-streng. På eldre Delphi-versjoner før overbelastningsoppløsning (overload resolution) for disse typene, er strengformen eksponert under navnet RtLTextOutStr med den identiske parameterlisten

Arbeidsdelingen mellom de to utdata-kallene er streng. TextOut tegner kodepunkter (codepoints) i den rekkefølgen du sender dem, noe som er riktig for latinsk, kyrillisk, og CJK, og feil for arabisk og hebraisk. RtLTextOut omorganiserer hver linje til visuell høyre-til-venstre rekkefølge først, og tegner deretter, og holder innebygde latinske ord og sifre til å leses fra venstre til høyre inne i linjen. HotPDF holder med vilje de to metodene adskilt i stedet for å gjette retning ut fra tegnene, så valget av hvilken du skal kalle er valget av hvilken skript-oppførsel (script behavior) du får; bruk RtLTextOut for høyre-til-venstre-kjøringer (runs), TextOut for alt annet, og rut aldri den ene gjennom den andre. Hvorfor omorganiseringen i det hele tatt eksisterer, hva den toveisede Unicode-algoritmen (Unicode Bidirectional Algorithm) og arabisk kontekstuell sammenføyning (contextual joining) faktisk gjør, og hvor HotPDFs forming (shaping) stopper er emnet for den medfølgende artikkelen om Arabisk og RTL tekstforming med HotPDF; alt nedenfor er det praktiske oppsettet

Diagram av hvordan RtLTextOut omorganiserer en blandet arabisk og latinsk linje til visuell høyre-til-venstre rekkefølge før den tegnes inn i en PDF
RtLTextOut omorganiserer hver linje til visuell rekkefølge før tegning: høyre-til-venstre-kjøringer beholder sekvensen sin mens innebygde latinske ord og sifre leses fra venstre til høyre i linjen.

Charset-argumentet bestemmer skriptet

Det som forteller RtLTextOut om den legger ut arabisk eller hebraisk, er ikke metoden, det er skrifttypen (font). SetFont tar et Windows-tegnsett (charset) som sitt fjerde argument, og den verdien bærer skript-reglene inn i høyre-til-venstre-kallet: 178 velger arabisk, 177 velger hebraisk. Sett charset, tegn deretter, og de to linjene under kommer ut i riktig leserekkefølge uten noen videre konfigurasjon

// Arabisk: charset 178 forteller RtLTextOut å bruke arabiske regler
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebraisk: charset 177 bytter reglene til hebraisk
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Én sekvensdetalj er lett å gå glipp av: SetFont må komme først og må gjentas etter hver AddPage, fordi den gjeldende skrifttypen, inkludert charset, ikke overlever et sideskift. Glemmer du å gjenta det, faller den andre siden tilbake til uansett hvilken skrifttype som var aktiv, noe som for arabisk vanligvis betyr tomme bokser

Den reverserer ikke tekst du allerede har reversert

Den ene feilen som sluker mest feilsøkingstid her, er å mate RtLTextOut med en streng du allerede har snudd (flipped) for hånd. Folk når denne metoden etter at et første forsøk med vanlig TextOut kom ut baklengs, og en vanlig nødløsning (stopgap) er å reversere tegnene i kode før de tegnes. RtLTextOut reverserer internt på egen hånd, så en forhåndsreversert streng blir reversert en gang til og lander rett tilbake der den startet. Send teksten i logisk rekkefølge, rekkefølgen du ville skrevet den og lest den høyt, og la kallet gjøre omorganiseringen (reordering)

Fellen (trap) er styggere enn en ren snuing, fordi en dobbeltreversert streng kan se riktig ut for en hel-arabisk testfrase og deretter bryte det øyeblikket en linje bærer et latinsk ord eller et tall. Inne i en høyre-til-venstre linje er det meningen at disse innebygde kjøringene (runs) skal leses fra venstre til høyre, og hånd-reversering ødelegger den nestingen, mens det rent arabiske tilfellet tilfeldigvis overlever det. Så feilen (bug) seiler gjennom din første røyktest (smoke test) og dukker opp senere på en ekte faktura med et kontonummer i den. Fjern hver eneste manuelle reversering i det øyeblikket du bytter til RtLTextOut

Direction-bivirkningen verdt å kjenne til

Å kalle RtLTextOut endrer mer enn linjen du tegner. Det snur (flips) også dokumentets leseretning-preferanse til høyre-til-venstre, det samme du ellers ville satt selv gjennom Direction-egenskapen. Denne setteren (setter) legger vpDirection til dokumentets ViewerPreferences, noe som forteller et visningsprogram (viewer) hvordan det skal arrangere to-opp (two-up) oppslag (spreads) og hvilken side en motstående-side layout (facing-page layout) starter fra. Når hele dokumentet er arabisk eller hebraisk er dette nøyaktig hva du vil ha, og du får det gratis

Det er verdt å vite om nettopp fordi det er usynlig på en enkeltside. Hvis dokumentet for det meste er venstre-til-høyre med en høyre-til-venstre blokk, vil det første RtLTextOut-kallet likevel vippe (tip) hele filens preferanse, og ingenting i ditt enkeltside-prøvetrykk (proof) vil vise det. Symptomet vises uker senere når noen skriver ut et tosidig hefte (duplex booklet) og oppslagene kommer ut speilet. Hvis det ikke er det du ønsker, setter du Direction eksplisitt tilbake etter høyre-til-venstre-kjøringen (run):

// RtLTextOut har allerede satt dokumentretningen til RightToLeft;
// gjenopprett venstre-til-høyre hvis dokumentet hovedsakelig er LTR
Pdf.Direction := LeftToRight;

For et dokument som genuint leses høyre-til-venstre, la det være. Poenget er å vite at kallet har en dokumentomfattende effekt, slik at hefte-overraskelsen aldri skjer

Registrer fonten du leverer, ikke den du håper er installert

Ingenting av omorganiseringen betyr noe hvis skrifttypen (font) ikke har noen glyffer å tegne. Den klassiske feilen er en rapport som gjengis feilfritt (flawlessly) på utviklerens maskin, hvor Arial Unicode MS tilfeldigvis er til stede, og kommer ut som rader av tomme bokser på en kundes server hvor Windows stille byttet den ut med en skrifttype med null arabisk dekning i det hele tatt. Kuren er å slutte å stole på installerte systemfonter og registrere en du leverer (ship) med applikasjonen

// Lever en kjent arabisk skrifttype og registrer den før du tegner
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

To grenser rir med på registrering (registration). En skrifttype hentet inn gjennom RegisterUnicodeTTF blir innebygd (embedded), og HotPDFs håndtering av innebygd Unicode trenger dokumentet på PDF 1.5 eller nyere; det biter bare hvis noe nedstrøms insisterer på PDF 1.4, men når det gjør det er feilen lydløs (silent). Det andre er juridisk snarere enn teknisk: TrueType-filer bærer biter for tillatelse for innebygging (embedding-permission bits), og en skrifttype (face) som ser bra ut på skjermen kan være lisensiert på en måte som forbyr å levere den inne i kundedokumenter. Bekreft lisensen før du bygger inn (embed), ikke etter en klage

Et komplett konsolleksempel

Ved å sette brikkene sammen, her er et selvstendig (self-contained) program som skriver én side med en arabisk linje, en hebraisk linje, og en blandet linje som bærer et latinsk produktnavn. Hver blokk setter sitt tegnsett (charset), og tegner deretter i logisk rekkefølge

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // HotPDF hovedenhet (main unit)

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

    // En latinsk overskrift går gjennom den vanlige TextOut-banen
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Høyre-til-venstre tekst med HotPDF');

    // Arabisk: charset 178, logisk rekkefølge, RtLTextOut gjør omorganiseringen
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

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

    // Blandet linje: det innebygde latinske ordet leses fortsatt venstre til høyre
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 640, 0,
      'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');

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

Kjør det og åpne resultatet. De arabiske og hebraiske linjene leses høyre til venstre, bokstavene føyes sammen (join) der skriptet (script) føyer dem sammen, og i den siste linjen sitter tokenet HotPDF venstre-til-høyre inne i den arabiske kjøringen (run). Den nestingen er det riktige toveisresultatet (bidirectional result), ikke en feil (bug), selv om førstegangs-anmeldere rutinemessig melder (file) det som en; formingsartikkelen (shaping article) lenket ovenfor forklarer hvorfor Unicode-reglene krever det, og hvordan du formulerer akseptkriteriene dine (acceptance criteria) slik at rapporten aldri blir innlevert

Vanlige feil og deres løsninger

Hver feil (failure) nedenfor har dukket opp i en ekte support-tråd, og hver enkelt sporer (traces) tilbake til en av seksjonene ovenfor

  • Utdata leses baklengs eller kodes opp (scrambles) på blandede linjer — strengen ble reversert for hånd før kallet, vanligvis en gjenværende løsning (workaround) fra et TextOut-forsøk. Slett hver manuell reversering og send i logisk rekkefølge; RtLTextOut reverserer internt
  • Bokstaver skrives ut frakoblet i isolerte former — teksten gikk gjennom vanlig TextOut, eller SetFont ble kalt uten et høyre-til-venstre-tegnsett (charset). Tegn med RtLTextOut og send 178 for arabisk eller 177 for hebraisk som det fjerde SetFont-argumentet
  • Tomme bokser på kundens maskin — Windows erstattet (substituted) en skrifttype (font) med null arabisk eller hebraisk dekning. Slutt å navngi installerte fonter; registrer en skrifttype (face) du leverer (ship) gjennom RegisterUnicodeTTF og bruk SetFont på det navnet
  • Andre side gjengis (renders) i feil skrifttype — den gjeldende skrifttypen overlever ikke AddPage. Gjenta SetFont-kallet, inkludert charset, etter hvert sideskift (page break)
  • Tosidige oppslag (Duplex spreads) skrives ut speilvendt på et hovedsakelig LTR-dokument — det første RtLTextOut-kallet snudde dokumentets Direction som en bivirkning. Sett Pdf.Direction := LeftToRight etter høyre-til-venstre-kjøringen (run)
  • Innebygd Unicode-tekst degraderes lydløst nedstrøms — noe i rørledningen (pipeline) tvinger frem PDF 1.4, og HotPDFs håndtering av innebygd Unicode trenger 1.5 eller nyere. Hev (raise) dokumentversjonen eller fjern nedstrøms-begrensningen (constraint)

Før formatet leveres (ships), bekreft utover visuell sjekk (eyeballing): kopier teksten tilbake ut av visningsprogrammet, kjør søket (search) i dokumentet, åpne filen på en maskin uten dine utviklingsfonter, og legg ett genuint dokument foran en innfødt (native) leser. Den fulle sjekklisten (verification checklist), dekningskartet per skript (per-script coverage map), og teststreng-korpuset (test-string corpus) verdt å bygge lever alt i den medfølgende artikkelen om Arabisk og RTL tekstforming med HotPDF

Kallene RtLTextOut, SetFont og RegisterUnicodeTTF vist her, er en del av HotPDF-komponenten for Delphi og C++Builder