Technical Article

Шрифтове и текст в PDF: Защо глифовете се превръщат в квадратчета

PDF файл, който изглежда перфектно на вашия компютър, но се рендерира като поредица от празни квадратчета на чужд, е най-често срещаният дефект с шрифтовете в софтуера за документи, и това почти никога не означава, че текстът е грешен. Символите са непокътнати, кодирането е наред, просто глифовете липсват. Разликата между двете машини се състои в това кои шрифтове са инсталирани в операционната система, а границата между преносим и уязвим файл зависи от едно решение, взето при записването на страницата: дали шрифтът е вграден в PDF документа, или се предполага, че е наличен на отсрещната страна.

За да разберем защо се случва това и защо друг тип грешка генерира текст, който изглежда годен за търсене, но при копиране се превръща в неразчитаеми знаци, трябва да разгледаме как PDF съхранява текст. Той не съхранява изречения. Той съхранява кодове на глифове, програма за шрифтове и таблици, които ги свързват, като всеки проблем с рендерирането или извличането се корени в разминаването между тези три елемента. Следва кратък преглед на тези механизми, базиран на стандарта ISO 32000, заедно с Delphi извикванията, които ги управляват.

Символи, кодове и глифове са три различни неща

Терминологията често обърква хората, тъй като в ежедневната реч три различни концепции се сливат в думата „букваâ€? Символът е абстрактна писмена единица, например идеята за главна буква А, идентифицирана в Unicode като U+0041. Глифът е начертана фигура, контурът от криви и линии, който даден шрифт използва за изобразяване на този символ. Между тях стои кодът: байтът или байтовете в потока от съдържание, които казват на визуализатора кой глиф от текущия шрифт да изрисува.

PDF работи с кодове. Когато потокът от съдържание показва низ, тези байтове са индекси в активния шрифт, а не в Unicode. Кодирането на шрифта определя, че код 65 означава „нарисувай глифа, записан под номер 65â€? и нищо в този процес не знае, че за човека резултатът изглежда като буквата А. Именно това позволява на PDF да се рендерира идентично навсякъде, където глифовете са налични, и това е причината извличането на текст да е отделен проблем от неговото показване: рисуването изисква само съпоставка код-към-глиф, докато четенето изисква код-към-Unicode, а това са две различни таблици, които могат да се разминават или да липсват независимо една от друга.

Типовете шрифтове, с които действително ще се сблъскате

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

Type 1 е оригиналният контурен формат PostScript на Adobe, изграден от кубични криви на Безие. Четиринадесетте стандартни шрифта, които всеки съвместим четец трябва да предостави (фамилиите Helvetica, Times, Courier, Symbol и ZapfDingbats), са Type 1 и речникът на шрифта, който посочва някой от тях, може законно да изключи самата програма за шрифта. Това е единственият случай, в който оставянето на шрифт без вграждане е безопасно по спецификация, а не по случайност. За всеки друг Type 1 шрифт програмата трябва да бъде вградена, в противен случай визуализаторът я замества с нещо друго, обикновено метрично подобен, но визуално различен шрифт.

TrueType използва квадратични криви и произлиза от света на Apple и Microsoft. Повечето системни шрифтове са такива и именно тях ще вграждате най-често. Обикновеният TrueType шрифт в PDF е ограничен до еднобайтови кодове, така че един такъв шрифт може да адресира най-много 256 глифа едновременно. Това ограничение е структурната причина, поради която CJK (китайски, японски, корейски) и други големи писмености не могат да се използват с обикновен шрифт.

Type 0, съставният или CID-keyed шрифт, е решението на това ограничение. Той използва многобайтови кодове и CMap за насочването им през дъщерен CIDFont, чиито контури са TrueType или CFF/Type 1. Това е единственият тип шрифт, който може да съдържа хиляди глифове, така че всеки PDF файл, съдържащ китайски, японски, корейски или богата многоезична смес, използва Type 0, независимо дали авторът е помислил за това. Компромисът е сложността: повече движещи се части, повече от които трябва да бъдат правилни както за рендерирането, така е и за извличането.

Един TrueType шрифт, рендериран при 12, 18, 24 и 36 пункта в PDF, показващ, че единичен вграден контур се мащабира до всякакъв размер

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

Вграждането е разликата между преносимост и уязвимост

Вграждането означава, че програмата за шрифта, тоест действителните контурни данни, се записват в PDF файла като поток. Четец на машина, която никога не е имала вашия шрифт, чете тези контури директно от файла и изчертава точните глифове. Ако пропуснете вграждането, вие залагате на това, че получателят разполага с шрифт със същото име; когато това не е така, визуализаторът се връща към заместващ шрифт. За стандартните четиринадесет това заместване е дефинирано и безопасно. За всичко останало то варира от леко разминаване с различен шрифт до празни квадратчета, когато никой заместващ шрифт не покрива съответната писменост.

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

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Compression := cmFlateDecode;
    Pdf.FontEmbedding := True;          // outlines travel inside the file
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Calibri', [], 11);
    Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Последователността не е козметична. BeginDoc е мястото, където HotPDF финализира структурата на документа, така че FontEmbedding трябва да бъде зададен на true преди това извикване. Ако го присвоите след това, няма да получите нито грешка, нито предупреждение, но файлът просто ще се запише без своите шрифтове. Това е най-лошият вид грешка: тя преминава всички тестове на машината на разработчика, където шрифтът е инсталиран, и се появява едва при клиента, където той липсва.

Вграждането е също така точката, в която лицензирането се среща с инженерството. Програмата за даден шрифт съдържа флагове, които описват дали той може да бъде вграждан свободно, само за предварителен преглед или изобщо не. Спазването на тези флагове е ваша отговорност, а не на визуализатора, и фактът, че „Ð?проработилоâ€? не означава, че е разрешено.

Подмножества (Subsetting): вграждане само на използваните глифове

Пълното вграждане записва цялата програма на шрифта във файла. Голям CJK TrueType шрифт може да достигне няколко мегабайта, а вграждането му изцяло само за показване на десетина символа е неефективно и се натрупва в многостранични документи. Използването на подмножества решава този проблем, като записва само глифовете, към които документът се отнася, и след това преименува шрифта с шестбуквен префикс и знак плюс (например ABCDEF+Calibri в списъка с шрифтове на всеки PDF с подмножество), така че четецът никога да не бърка частично вградения шрифт с пълен системен шрифт със същото име.

За повечето генерирани документи използването на подмножества е най-подходящият избор по подразбиране. Това поддържа размера на файла пропорционален на съдържанието, а не на оригиналния шрифт, което е изключително важно за големи многоезични шрифтове, които иначе биха доминирали във файла. Единственото предупреждение е, че подмножеството съдържа само това, което е било използвано по време на създаването. Ако последващ процес се опита да добави текст с подмножествен шрифт по-късно, глифовете, които са му необходими, може да липсват във файла, което представлява сериозно ограничение при частично редактиране на чужд PDF документ.

Unicode шрифтове и проблемът с квадратчетата при CJK

Когато текстът не е на латиница, възможностите на обикновените шрифтове се изчерпват и решението е изрично да регистрирате шрифт с Unicode поддръжка, което позволява на HotPDF да изгради Type 0 шрифт от него. RegisterUnicodeTTF зарежда TrueType файл по път; след това регистрираното име може да се използва в SetFont както всяко друго:

Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世ç•?こんにちã?안녕하세ìš?);
Pdf.EndDoc;

Два фактора са решаващи за успеха на тази операция. Шрифтът трябва да покрива съответните писмености в низа: TrueType шрифт, поддържащ само латиница, няма да генерира китайски глифове само защото сте поискали това, и резултатът отново ще бъде празни квадратчета (този път защото глифът наистина липсва в този шрифт). Освен това вграждането трябва да остане включено, тъй като Type 0 шрифт, сглобен от регистриран TTF, е неизползваем за четец, който не може да намери контурите. За смесено съдържание най-надеждният избор е шрифт с широко покритие, като например фамилиите Noto и Arial Unicode MS, вградени и подмножествени.

Писменостите с посока на писане от дясно на ляво и сложните писмености добавят слой за оформяне (shaping) над покритието. HotPDF предлага метода RtLTextOut за арабски и иврит, който управлява промяната на посоката, така че вие подавате логическия ред на символите, а библиотеката ги подрежда на екрана. Правилното изобразяване на арабски език изисква покритие плюс оформяне плюс посока (три отделни неща), като квадратче там може да означава проблем с всяко едно от тях.

Таблицата ToUnicode: откъде идва възможността за копиране и поставяне

Всичко дотук се отнасяше за изчертаването. Извличането е огледален процес и се проваля по свои собствени причини. Визуализаторът рендерира страницата, като използва съпоставката код-към-глиф на шрифта, но когато потребителят избере текст и го копира, визуализаторът трябва да превърне същите тези кодове обратно в Unicode. Това обратно съпоставяне е ToUnicode CMap, който представлява незадължителен поток, прикрепен към шрифта.

Когато тя е налична и правилна, копираният текст излиза с правилните символи. Когато липсва или е сгрешена, или шрифтът е бил подмножествен с персонализирани кодове на глифове без записана ToUnicode таблица, страницата изглежда перфектно, но клипбордът се пълни с неразчетими символи: кодовете на глифовете се четат така, сякаш са Unicode, което не е така за персонализирано кодирано подмножество. Това е причината, поради която сканиран документ със слой за OCR разпознаване на текст може да позволява търсене, докато оригинален цифров PDF, създаден от невнимателен генератор, не позволява. Рендерирането и извличането се основават на различни таблици, така че один файл може да удовлетворява едното изискване, но да се проваля при другото. Ако извличането на текст е важно за вашия изходен резултат, разглеждайте коректната ToUnicode таблица като задължително изискване и я проверете чрез копиране на текст от мостра, вместо да вярвате сляпо, че тя присъства.

Как да диагностицирате бързо грешка с шрифтовете

Начинът, по който се проявява проблемът, ви подсказва къде да търсите. Празните квадратчета на друга машина почти винаги означават невграден шрифт, затова първо проверете вграждането, а след това покритието на глифовете. Квадратчетата, които се появяват дори на вашата собствена машина, сочат към покритието: шрифтът не съдържа тази писменост, независимо от вграждането. Текстът, който се рендерира правилно, но се копира като безсмислица, е проблем с ToUnicode, а не с рендерирането, и промените по шрифтовете или вграждането няма да го решат, тъй като изчертаването никога не е било счупено. За да проверите готов файл, го отворете в Acrobat и прегледайте Document Properties, Fonts: коректният запис показва типа, указва Embedded или Embedded Subset и дава името на кодирането. Шрифт, който трябва да бъде вграден, но не е, се вижда там още преди клиентът да ви е съобщил за това.

Нищо от това не е необичайно, след като разберете разликата между символ, код и глиф. Вграждайте шрифтовете, с които чертаете, използвайте подмножества за големите, изберете Unicode шрифт и използвайте RegisterUnicodeTTF веднага щом текстът излезе извън латиницата, и поддържайте правилна ToUnicode таблица, ако се предполага извличане на текста. Осигурете тези неща и квадратчетата ще изчезнат. За по-подробно запознаване с механизмите около тези процеси, статията за анатомията на минимален PDF показва къде се намира речникът на шрифта в дървото от обекти, а структурният преглед на документи обяснява как ресурсите се споделят между страниците.

Показаните тук извиквания на SetFont, FontEmbedding и RegisterUnicodeTTF са част от HotPDF Component за Delphi и C++Builder.