Teknisk artikel

CJK vertikal text i PDF med Delphi och HotPDF

Sätt en japansk roman på sidan och det första du märker är att texten löper nedåt i kolumnen, inte längs raden, och att kolumnerna fortskrider från höger kant av arket mot vänster. En läsare som vuxit upp med det tycker att horisontell text verkar en aning klinisk. Det ingenjörsmässiga problemet är att PDF, liksom nästan alla digitala textsystem, byggdes kring en horisontell baslinje som växer från vänster till höger, och en innehållsström har inget begrepp om "skriv detta stycke nedåt istället". Så när ett Delphi-program ska producera ett certifikat, en dikt, en skylt, eller ett traditionellt utformat juridiskt dokument för en taiwanesisk, japansk, eller koreansk läsare, måste layouten monteras för hand: ett tecken under nästa, en kolumn till vänster om den senaste

HotPDF ger dig en växel som sköter bokföringen för varje tecken åt dig. Typsnittet du ställer in bär en IsVertical-flagga, och när den väl är påslagen staplar ett enda TextOut-anrop en hel sträng i en vertikal kolumn istället för att köra den längs en baslinje. Placeringen av kolumnen, ordningen från höger till vänster, och en tyst viktig glyfersättning är vad resten av den här sidan går igenom

A4 PDF-sida producerad av HotPDF som visar kinesisk, japansk och koreansk text staplad i vertikala kolumner som läses från höger till vänster
En enskild A4-sida med kinesisk, japansk och koreansk text i vertikala kolumner, genererad från en Delphi-procedur.

Växeln finns på SetFont

Vertikal layout är inte en egenskap hos sidan eller dokumentet. Det är en egenskap hos det typsnitt du ritar med, och du slår på den med det femte argumentet i SetFont:

// SetFont(FontName, FontStyle, Size, FontCharset, IsVertical)
// Det femte argumentet växlar det aktuella typsnittet till vertikalt läge.
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, False); // horisontellt
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, True);  // vertikalt

Eftersom flaggan vilar på typsnittet, växlar du mellan horisontell och vertikal skrivning helt enkelt genom att anropa SetFont igen med ett annat sista argument. En sida kan ha en horisontell titel tvärs över toppen och vertikal brödtext därunder, och HotPDF håller isär de två lägena per typsnittsobjekt istället för per sida. Det är detta som gör blandade layouter möjliga utan någon speciell lägeshantering på din sida: varje TextOut efter ett vertikalt SetFont staplas, varje TextOut efter ett horisontellt löper längs baslinjen, och det sista SetFont vinner

Det fjärde argumentet är Windows teckenuppsättning, samma som ett horisontellt anrop tar. Att skicka DEFAULT_CHARSET låter systemet lösa glyfer per sträng, vilket har betydelse här eftersom ett vertikalt dokument ofta blandar skript. Allt annat med typsnittet gäller fortfarande: det måste vara installerat på byggmaskinen, och du vill nästan alltid ha FontEmbedding := True så att filen renderar samma CJK-glyfer hos en läsare som aldrig haft Arial Unicode MS

Ett TextOut-anrop är en kolumn

Med ett vertikalt typsnitt aktivt sprider ett TextOut-anrop inte längre sin sträng i sidled från den angivna punkten. Det planterar det första tecknet i toppen och förflyttar resten rakt nedåt, och avancerar med typsnittets radhöjd för varje glyf. Det X du skickar fixerar kolumnen; det Y du skickar fixerar var toppen av kolumnen börjar. För att lägga ut en riktig passage utför du därför en TextOut per kolumn och stegar X åt vänster mellan anropen, eftersom CJK-kolumner läses från höger till vänster

var
  Pdf: THotPDF;
const
  ColTop = 760;   // y för den första glyfen i varje kolumn (punkter)
  ColGap = 28;    // horisontellt avstånd mellan kolumner
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'VerticalText.pdf';
    Pdf.FontEmbedding := True;       // bädda in CJK-typsnittet för bärbar rendering
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := psA4;

    // En horisontell rubrik först, i det vanliga skrivläget.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 16, DEFAULT_CHARSET, False);
    Pdf.CurrentPage.TextOut(60, 800, 0, 'Tang poem, vertical layout');

    // Växla typsnittet till vertikalt läge; varje TextOut nedan staplas nu.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 18, DEFAULT_CHARSET, True);

    // Kolumner avancerar från höger till vänster, så X minskar med varje anrop.
    Pdf.CurrentPage.TextOut(520, ColTop, 0, '床前明月光');
    Pdf.CurrentPage.TextOut(520 - ColGap,     ColTop, 0, '疑是地上霜');
    Pdf.CurrentPage.TextOut(520 - ColGap * 2, ColTop, 0, '舉頭望明月');
    Pdf.CurrentPage.TextOut(520 - ColGap * 3, ColTop, 0, '低頭思故鄉');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Dikten läses som den ska: den högraste kolumnen först, uppifrån och ner, sedan hoppar ögat till vänster till nästa kolumn. Inget i själva strängen kodar den ordningen. Du kodar den i X-koordinaterna, och minskar dem anrop för anrop. Får du riktningen felvänd så blir versen omvänd, vilket är det i särklass vanligaste misstaget vid portering av en horisontell layout till vertikal

Koordinater: nedåt sidan, höger till vänster

Två axlar är i spel och de drar åt motsatta håll, så det lönar sig att vara exakt. HotPDF mäter från det nedre vänstra hörnet av sidan, med Y ökande uppåt, i punkter. En vertikal kolumn börjar därför vid ett högt Y (nära toppen av arket) och tecknen sjunker därifrån när HotPDF subtraherar en radhöjd för varje tecken. Du ställer in det start-Y:et en gång per kolumn och komponenten hanterar nedstigningen

Den horisontella axeln är den du driver manuellt. Varje kolumn sitter på sitt eget X, och påföljande kolumner stegar till mindre X-värden eftersom läsordningen är från höger till vänster. En förnuftig rytm är att välja X för den högraste kolumnen, och sedan subtrahera en fast kolumnlucka för varje efterföljande anrop, som exemplet gör med ColGap. Du väljer själv luckan; för tätt och intilliggande kolumner vidrör varandra, för glest och blocket ser tomt ut. För brödtext på 12 till 18 punkter läses en lucka lite större än teckenstorleken bekvämt

Glyfer som måste byta form

Ett fåtal tecken är inte bara roterade kopior av sina horisontella själv; de måste bytas ut när texten blir vertikal. Den som HotPDF hanterar åt dig är den japanska markeringen för förlängt ljud (U+30FC), den långa vokalstapeln som visas i katakanaord som コーヒー. Ritad horisontellt är den ett kort streck på baslinjen. Om du staplade samma glyf i en kolumn skulle den ligga platt över kolumnen, vilket är fel: i vertikal japanska blir markeringen ett vertikalt streck som förbinder de två tecknen den sitter emellan. HotPDF upptäcker U+30FC i den vertikala banan och renderar den som en vertikal stapel (U+007C) så att lång-vokalmarkeringen pekar på rätt håll utan något arbete från din sida

Den enda ersättningen täcker fallet som förstör de flesta naiva implementeringar, men det är värt att veta var det allmänna problemet går djupare. Fullständig vertikal typografi roterar även latinska bokstäver och västerländsk interpunktion nittio grader, flyttar små kana, och omplacerar hakparenteser och kommatecken till sina vertikala former, och en komplett implementering av det ligger i ett typsnitts OpenType-vertikala funktioner snarare än i fasta regler. HotPDF kan utnyttja dessa funktioner när typsnittet har dem: vertikala alternativ (vert och vrt2 GSUB-funktionerna) och vertikal kerning (vkrn och vpal GPOS-uppslagningar) är frivilliga, tillämpas längs den vertikala banan endast när det aktiva typsnittet faktiskt definierar dem. För blandad CJK-text i ett enda bred-täckande teckensnitt som Arial Unicode MS räcker den inbyggda hanteringen av U+30FC plus ett jämnt radhöjdssteg för att producera korrekta, läsbara kolumner; OpenType-funktionerna har betydelse när du flyttar till ett typsnitt designat för fin vertikal sättning och vill ha dess inhemska kana-positionering och avstånd mellan glyfer

Att blanda skript och riktningar på en sida

Verkliga dokument är sällan rena. En vertikal japansk sida kan bära en horisontell engelsk bildtext, ett sidnummer längs botten, eller ett block av koreanska satt vertikalt bredvid japanskan. Eftersom den vertikala flaggan är en typsnittsnivåväxel, komponerar du dessa genom att växla SetFont-anrop istället för att hantera något sidomfattande tillstånd. Ställ in ett horisontellt typsnitt, skriv den löpande rubriken och folion, ställ in ett vertikalt typsnitt, lägg ut kolumnerna, ställ in ett horisontellt typsnitt igen för en sidfot. Varje region tar upp läget från det senaste SetFont, så den enda disciplin som krävs är att anropa det närhelst du byter riktning

En detalj att planera för när skript blandas: kinesiska, japanska och koreanska ideogram är nära fyrkantiga och staplas på ett jämnt steg, men latinska körningar inbäddade i en vertikal kolumn har inte den enhetliga frammatningen. Om du behöver några latinska ord inuti en annars vertikal text, bestäm medvetet om de ska roteras för att löpa nedåt i kolumnen eller sättas upprätt som ett kort horisontellt infällt block, och positionera det fragmentet med sitt eget TextOut istället för att låta det rida på det vertikala steget avsett för ideogram. Att behandla den blandade körningen som sitt eget placeringsproblem håller kolumnrytmen intakt

Från exempel till produktion

Bitarna är små och de komponerar sig förutsägbart. Slå på den vertikala flaggan i SetFont, ge ut ett TextOut per kolumn från ett fast topp-Y, och minska X över anrop så att kolumnerna läses från höger till vänster. Bädda in typsnittet så att CJK-glyferna överlever resan till en läsares maskin, och låt komponenten hantera U+30FC-lång-vokalmarkeringen och, där typsnittet stöder dem, OpenType-vertikala funktionerna. Därifrån är produktionsanpassningen av layouten mestadels aritmetik: härleda kolumners X-positioner från en uppmätt blockbredd, bryta långa passager till kolumner som passar sidhöjden, och reservera utrymme för horisontella rubriker och folios

För den bredare text- och typsnittsytan som detta bygger på, inklusive horisontella TextOut-konventioner och Unicode-typsnittsregistrering, se flerspråkiga Hello World-exemplet och TextOut-exemplet. Den vertikala SetFont-växeln och de TextOut-anrop som visas här ingår i HotPDF Component för Delphi och C++Builder