Техническа статия

Пълно двустранно подравняване за PDF текст в Delphi с HotPDF

Пълното двустранно подравняване е оформлението, което кара колона от текст да се изравни както на левия, така и на десния ръб, видът, който очаквате от печатна книга или официален отчет. То е лесно за описване и изненадващо лесно да се сбърка, тъй като отговорът на въпроса "къде отива допълнителното място" не е същият за английски, както е за японски, и защото наивният начин за измерване на всеки ред превръща бързата страница в бавна. HotPDF ви дава съобразено със скрипта двустранно подравняване чрез едно-единствено извикване за оформление на кутия, а под това извикване стои класическо подобрение на производителността, което си струва да се разбере само по себе си

Тази статия разглежда и двете. Първо, типографското правило, което решава как се разпределя свободното пространство за скриптове с интервали между думите спрямо скриптове без тях. Второ, промяната в измерването, която намали разходите за двустранно подравняване на страница приблизително осемдесет пъти без видима разлика в изхода. И двете имат значение, ако генерирате документи в голям обем и искате те да се четат като истински набор, а не като моноширинен изход, разтегнат, за да се побере

Какво всъщност изисква пълното двустранно подравняване

Ред текст, начертан с естествената си ширина, почти никога не достига десния ръб на своята колона. Винаги има остатък, свободно пространство, между мястото, където завършва последният глиф, и мястото, където се намира границата на колоната. Лявото подравняване оставя това свободно пространство отдясно. Дясното подравняване го премества наляво. Центрирането го разделя. Пълното двустранно подравняване го премахва, като разширява самия ред, докато и двата ръба срещнат кутията, и единственият честен начин да направите това е да изтласкате глифовете един от друг отвътре

Правилото, което отличава доброто двустранно подравняване от лошото, е къде поставяте свободното пространство. Скрипт, който изписва думи с интервали между тях, като английски и останалата част от латинското семейство, има естествени шевове при всяко разстояние между думите. Разширяването на тези интервали е невидимо за окото, тъй като читателите вече приемат, че разстоянията между думите варират. Скрипт, който пише без интервали между думите, като китайски Han йероглифи, японска kana или корейски Hangul, няма такива шевове. Там свободното пространство трябва да бъде разпределено равномерно между съседните глифове, което е принципът, който японските словослагатели наричат kintou-waritsuke, равномерно разстояние. Поставянето на разтягане на интервали между думи в латински стил на CJK ред или натъпкването на цялото свободно пространство в едното място, където CJK ред случайно съдържа интервал, създава реките и празнините, които маркират любителски изход

Как HotPDF решава къде отива пространството

HotPDF взема това решение за всеки интервал, а не за всеки ред. Когато подравнява двустранно ред, той обхожда всяка съседна двойка глифове и пита дали между тях седи разтеглива граница. Една граница е разтеглива, когато от която и да е страна има интервал или табулация, латинският случай, или когато и двете страни са CJK знаци, които могат да се пренасят, случаят с равномерно разстояние. Той брои тези граници, разделя свободното пространство на реда поравно между тях и добавя този дял към всеки квалифициран интервал

Следствието отпада естествено. Английски ред има разтегливи граници само в интервалите на думите си, така че цялото свободно пространство попада там и думите се раздалечават, докато буквите вътре във всяка дума запазват естественото си разстояние. Han или kana ред има разтеглива граница между почти всяка двойка глифове, така че свободното пространство се разпределя равномерно по целия ред, точно равномерното разстояние между глифовете, което тези скриптове изискват. Ред, който е една дълга латинска дума без вътрешен интервал, изобщо няма разтеглива граница, така че HotPDF го оставя с естествената му ширина, вместо да разкъсва думата буква по буква. Същата логика се справя със смесени латински и CJK пасажи в един ред без специални случаи, тъй като решението е локално за всяка граница

Една граница е умишлено изключена навсякъде. Позицията след последния глиф на даден ред никога не се третира като интервал, тъй като разтягането там просто би въвело отново десен остатък, което е обратното на двустранно подравняване

Защо последният ред се оставя на мира

Последният ред на параграфа е специален и грешката с него е най-често срещаният бъг при двустранно подравняване. Последният ред на параграфа обикновено е къс, често само няколко думи, и разтягането му до пълната ширина на колоната влачи тези думи през страницата в рядък, накъсан ред. Правилната типография оставя последния ред с естествената му ширина, подравнен наляво

HotPDF открива крайния ред по позиция. Докато пренася текста в редове, той знае кога редът, който току-що е отделил, достига края на предоставения низ. Този последен ред се излъчва с обикновено ляво подравняване и запазва естествената си ширина. Всеки ред преди него е двустранно подравнен към двата ръба. Твърдите прекъсвания на редове, които пишете в текста, се зачитат така, както са написани, така че умишлено къс ред също никога не се разтяга. Читателят вижда чист правоъгълен блок от текст, чийто последен ред завършва естествено, което е това, което окото очаква

Цената на измерване, която направи двустранното подравняване бавно

За да подравните двустранно ред, трябва да знаете точната му ширина и трябва да знаете аванса (advance) на всеки глиф, за да можете да поставите допълнителното пространство прецизно. Първата реализация получи тези числа по очевидния начин. Тя измерваше целия ред с пълна заявка за Unicode ширина, след това измерваше префикс след префикс, за да възстанови аванса на всеки глиф чрез изваждане. За ред от N глифа това са N+1 извиквания към двигателя за измерване и всяко извикване е пълно двупосочно GDI пътуване, искащо от операционната система да оформи и измери текста и да върне отговора обратно

На ред това звучи евтино. За цяла страница не е. Вземете плътна A4 страница с основен текст, приблизително четиридесет и пет реда от около осемдесет знака всеки. При N+1 двупосочни пътувания на ред това са около 81 двупосочни пътувания за всеки ред и приблизително 3645 за страницата, почти всички от които изразходвани за повторно измерване на текст, който двигателят вече е разгледал мигове по-рано. При пакетна задача, произвеждаща хиляди страници, тези режийни разходи доминират времето за оформление и всяко двупосочно пътуване пресича границата между вашия процес и графичната подсистема

Едно извикване вместо N плюс едно

Поправката е от онзи вид промяна, която изглежда малка и се изплаща много. GDI вече може да отчете общата ширина на низ и позицията на всеки глиф в една заявка. HotPDF излага това чрез GetWideCharAdvances, което запълва масив с естествения аванс на всеки глиф, включително кърнинг, и връща общата ширина, в едно извикване, а не в N+1. Рутината за двустранно подравняване, _HPDFEmitJustifiedWideLine вътрешно, иска всички аванси веднъж, изчислява свободното пространство, разпределя го през разтегливите граници и излъчва реда

За същата A4 страница измерването на ред спада от около 81 двупосочни пътувания до едно, така че страницата пада от приблизително 3645 двупосочни пътувания до около 45, близо осемдесеткратно намаление. Изходът е идентичен байт по байт, защото нищо в измерването не се е променило, освен колко пъти се изисква. Същият GDI двигател, същите метрики на шрифта, същият кърнинг захранват едни и същи числа. Само броят на двупосочните пътувания спадна. Когато едно измерване вече е правилно, правилната оптимизация е да спрете да го искате многократно, а не да го приближавате

Как редът достига до страницата

След като свободното пространство е разпределено, HotPDF излъчва реда с ExtTextOut и масив от аванси за всеки глиф, масива Dx. Всеки запис е разстоянието от началото на един глиф до следващия, което е естественият аванс на този глиф плюс неговия дял от свободното пространство, когато го следва разтеглива граница. Това се картографира директно върху модела на изображения в PDF. Позиционираният текст се записва с оператора TJ, масив, който редува поредици от глифове с изрични хоризонтални корекции, и стойностите на Dx стават точно тези корекции. Ето защо допълнителното място попада между глифовете в прецизни позиции на части от пункта, а не се фалшифицира със знаци за допълване, и защо двустранно подравнен ред на HotPDF се измерва правилно, ако инструмент надолу по веригата го прочете обратно

Вие не извиквате ExtTextOut сами за двустранно подравнени параграфи. Входната точка е WideTextOutBox, която обвива Unicode низ в кутия и прилага подравняването, което искате. Тя разделя текста на редове, които се побират в ширината на кутията, поставя всеки ред надолу по височината на кутията и връща броя знаци, които е успяла да побере, преди да изчерпи вертикалното пространство. Подравняването се избира от изброяването за двустранно подравняване

type
  THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);

Първите три са самообяснителни ляво, центрирано и дясно подравняване. Четвъртото, jtJustify, е пълното двустранно подравняване до двата ръба, описано тук, и това е стойността, която WideTextOutBox чете, за да включи съобразеното със скрипта разстояние

Двустранно подравняване на параграф на практика

Пълен пример създава документ, задава шрифт и излива параграф в кутия с пълно двустранно подравняване. Същият код подравнява двустранно латински и CJK текст без промяна на флаг, тъй като съобразяването със скрипта живее под API-то

uses
  HPDFDoc;

procedure JustifyParagraph;
var
  Pdf: THotPDF;
  Body: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Justified.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', 11);

    Body :=
      'Full justification spreads the slack on each filled line so both ' +
      'edges meet the column, while the last line keeps its natural width. ' +
      'For scripts with word gaps the space lands between words; for ' +
      'scripts without them it spreads evenly between glyphs.';

    // X, Y, LineSpacing, BoxWidth, BoxHeight, Text, Align
    Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);

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

За да начертаете същия блок ляво подравнен, центриран или дясно подравнен, променете само последния аргумент на jtLeft, jtCenter или jtRight. Пренасянето, поставянето на редове и върнатата стойност остават същите. Измерената ширина, която движи и четирите пътя, идва от GetWideTextWidth, заявката за ширина, съобразена с Unicode, която измерва WideString правилно там, където по-старото измерване по байтове би оразмерило погрешно всичко след Latin-1, което е това, което кара кутията да пренася CJK и текст със сурогатни двойки на правилното място на първо време

Двустранното подравняване е един слой от по-голям стек за оформяне на текст. Когато даден ред съдържа скриптове, които пренареждат или свързват своите глифове, решенията за разстояние тук седят върху работата, описана в нашата статия за оформяне на текст със сложен скрипт, а когато даден шрифт носи типографски варианти, които искате да изберете, вижте как да управлявате стилистични алтернативи на OpenType GSUB. Всичко това се доставя в HotPDF Component за Delphi и C++Builder, заедно с по-широките API-та за текст, оформление и документи, разгледани в този блог