Повне вирівнювання – це верстка, яка вирівнює стовпець тексту по обидва краї: лівий і правий, – вигляд, характерний для друкованих книг або офіційних звітів. Його легко описати і напрочуд легко зробити неправильно, адже відповідь на питання «куди йде додатковий пробіл» не однакова для англійської та японської мов, і тому що наївний спосіб вимірювання кожного рядка перетворює швидку сторінку на повільну. HotPDF надає вирівнювання з урахуванням скрипту через єдиний виклик верстки блоку, а під цим викликом лежить класична оптимізація продуктивності, варта розуміння сама по собі
Ця стаття охоплює обидва аспекти. По-перше, типографічне правило, яке визначає, як розподіляється слабина для скриптів з пробілами між словами порівняно зі скриптами без них. По-друге, зміна вимірювання, яка скоротила витрати на вирівнювання приблизно у вісімдесят разів без будь-якої видимої різниці у вихідних даних. Обидва аспекти важливі, якщо ви генеруєте документи у великому обсязі і хочете, щоб вони читалися як справжній набір, а не як моноширинний вивід, розтягнутий для заповнення
Що насправді вимагає повне вирівнювання
Рядок тексту, намальований у природній ширині, майже ніколи не досягає правого краю свого стовпця. Між кінцем останнього гліфа та межею стовпця завжди є залишок – слабина. Вирівнювання по лівому краю залишає цю слабину праворуч. Вирівнювання по правому – переміщує її ліворуч. Вирівнювання по центру ділить її навпіл. Повне вирівнювання прибирає її, розширюючи сам рядок до тих пір, поки обидва краї не торкнуться поля, і єдиний чесний спосіб зробити це – розсунути гліфи зсередини
Правило, яке відрізняє хороше вирівнювання від поганого, полягає в тому, куди поміщати слабину. Скрипт, що пише слова з пробілами між ними – наприклад, англійська та решта латинської сім'ї – має природні шви в кожному міжслівному просторі. Розширення цих пробілів непомітне для ока, бо читачі вже сприймають варіативність міжслівних відстаней. Скрипт, що пише без пробілів між словами – наприклад, китайські ієрогліфи Хань, японська кана або корейський Хангиль – не має таких швів. Там слабина повинна рівномірно розподілятися між сусідніми гліфами – це принцип, який японські складачі називають кінтоу-варіцуке, рівномірний розподіл. Застосування латинського розтягування пробілу між словами до рядка CJK або набивання всієї слабини в одне місце, де рядок CJK випадково містить пробіл, дає «ріки» та прогалини, що характерні для аматорського виводу
Як HotPDF вирішує, куди йде пробіл
HotPDF приймає це рішення для кожного проміжку, а не для кожного рядка. Вирівнюючи рядок, він обходить кожну суміжну пару гліфів і запитує, чи знаходиться між ними розтяжна межа. Межа є розтяжною, якщо з одного боку – пробіл або табуляція (латинський випадок), або якщо обидва боки – символи CJK, що допускають розрив (випадок рівномірного розподілу). Він рахує ці межі, рівномірно ділить слабину рядка між ними та додає свою частку до кожного відповідного проміжку
Наслідки виникають природньо. Англійський рядок має розтяжні межі лише в пробілах між словами, тому вся слабина потрапляє туди і слова розходяться, тоді як літери всередині кожного слова зберігають природне відстань. Рядок ієрогліфів Хань або кани має розтяжну межу майже між кожною парою гліфів, тому слабина розподіляється рівномірно по всьому рядку, – саме рівномірний міжгліфний розподіл, якого вимагають ці скрипти. Рядок, що складається з одного довгого латинського слова без внутрішнього пробілу, не має жодної розтяжної межі, тому HotPDF залишає його природньою шириною, не розриваючи слово по літерах. Та сама логіка обробляє змішані серії латиниці та CJK в одному рядку без особливої обробки, адже рішення є локальним для кожної межі
Одна межа навмисно виключається повсюди. Позиція після останнього гліфа рядка ніколи не вважається проміжком, бо розтягнення там лише відновило б залишок з правого боку, що є протилежністю вирівнювання
Чому останній рядок залишають у спокої
Останній рядок абзацу є особливим, і неправильне його оброблення – найпоширеніша помилка вирівнювання. Останній рядок абзацу зазвичай короткий, нерідко лише кілька слів, і розтягнення його до повної ширини стовпця розтягує ці слова по сторінці в рідкий, розбитий ряд. Правильна типографіка залишає останній рядок природньою шириною, вирівняний по лівому краю
HotPDF визначає завершальний рядок за позицією. Переносячи текст на рядки, він знає, коли щойно відокремлений рядок досягає кінця поданого рядка. Цей останній рядок виводиться зі звичайним вирівнюванням по лівому краю і зберігає природню ширину. Кожен рядок перед ним вирівнюється по обох краях. Жорсткі переноси рядків, що ви вписуєте в текст, шануються в тому вигляді, в якому вони написані, тому навмисно короткий рядок теж ніколи не розтягується. Читач бачить чистий прямокутний блок тексту, останній рядок якого закінчується природньо, що саме й очікує від цього погляд
Витрати на вимірювання, що робили вирівнювання повільним
Щоб вирівняти рядок, ви повинні знати його точну ширину, і ви повинні знати крок кожного гліфа, щоб точно розмістити додатковий пробіл. Перша реалізація отримувала ці числа очевидним способом. Вона вимірювала весь рядок повним запитом ширини Unicode, потім вимірювала кожен префікс по черзі, щоб відновити крок кожного гліфа шляхом різниці. Для рядка з N гліфами це N+1 звернень до рушія вимірювання, і кожне звернення – це повний цикл GDI, що запитує операційну систему оформити і виміряти текст та повернути відповідь
На один рядок це звучить дешево. Для сторінки – ні. Візьміть щільну сторінку A4 тексту тіла: приблизно сорок п'ять рядків приблизно по вісімдесят символів кожен. При N+1 зверненнях на рядок – це близько 81 звернення для кожного рядка і приблизно 3 645 для сторінки, майже всі витрачені на повторне вимірювання тексту, на який рушій щойно подивився. У пакетному завданні, що виробляє тисячі сторінок, ці витрати домінують у часі верстки, і кожне звернення перетинає межу між вашим процесом і підсистемою графіки
Один виклик замість N плюс один
Виправлення – це тип змін, який виглядає дрібним, але дає великий виграш. GDI вже може повідомляти загальну ширину рядка та позицію кожного гліфа в одному запиті. HotPDF надає це через GetWideCharAdvances, що заповнює масив природнім кроком кожного гліфа, включно з кернінгом, і повертає загальну ширину за один виклик замість N+1. Підпрограма вирівнювання, внутрішньо _HPDFEmitJustifiedWideLine, запитує всі кроки один раз, обчислює слабину, розподіляє її по розтяжних межах і виводить рядок
Для тієї самої сторінки A4 кількість вимірювань на рядок падає з приблизно 81 звернення до одного, тому кількість звернень для сторінки скорочується з приблизно 3 645 до близько 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 тексту, верстки та документів, розглянутими в цьому блозі