Витягніть емодзі чи японське ім'я з сімейного реєстру з PDF як текст, і на виході з'явиться прямокутник, знак питання чи взагалі нічого там, де мав бути символ. Character[] компонента PDFium — зазвичай причина цього: він читає кожен гліф через FPDFText_GetUnicode, що повертає повну кодову точку Unicode як беззнакове 32-бітне значення, а потім відкриває його Delphi як один 16-бітний WideChar. Будь-яка кодова точка за межами U+FFFF не може здійснити цю подорож одним шматком, і пошкодження ніколи не проявляється, поки ви дивитеся на відрендерену сторінку, бо рендеринг та вилучення тексту йдуть різними шляхами коду в PDFium — документ може ідеально показувати свої емодзі й усе одно видавати вам сміття тієї миті, коли ви читаєте Character[] у циклі й будуєте з нього рядок
Базова багатомовна площина і чому WideChar зупиняється на U+FFFF
WideChar у Delphi — 16-бітний тип, що може тримати лише одну одиницю коду UTF-16. Базова багатомовна площина (BMP) Unicode, діапазон U+0000 до U+FFFF, точно вписується туди, тому латиниця, кирилиця, грецька та звичайний блок об'єднаних ідеографів CJK усі проходять туди-й-назад через єдиний WideChar без інциденту. Дві родини символів регулярно опиняються поза цим у реальних документах: емодзі, багато з них у блоці Emoticons, що починається з U+1F600, та рідкісні ідеографи CJK з розширення B об'єднаних ідеографів CJK, діапазон U+20000 до U+2A6DF, зарезервований для менш поширених китайських, японських і корейських символів, включно з багатьма особистими іменами та назвами місць. UTF-16 обробляє все понад U+FFFF сурогатною парою — дві 16-бітні одиниці коду, старший сурогат у діапазоні $D800 до $DBFF, за яким іде молодший сурогат у $DC00 до $DFFF, що разом кодують одну кодову точку, — і математика за цим парюванням достатньо фіксована, щоб продемонструвати її прямо в Pascal
function ToSurrogatePair(CodePoint: LongWord; out Hi, Lo: WideChar): Boolean;
var
V: LongWord;
begin
Result := CodePoint > $FFFF;
if Result then
begin
V := CodePoint - $10000;
Hi := WideChar($D800 + (V shr 10));
Lo := WideChar($DC00 + (V and $3FF));
end;
end;
Подайте U+1F600, емодзі усмішки, через цю функцію, і результат — старший сурогат $D83D та молодший сурогат $DE00, два 16-бітні значення, не одне. Жодна половина нічого не означає сама по собі; самотній $D83D у рядку без $DE00 позаду нього — висячий сурогат, і більшість коду обробки тексту, що з ним стикається, або скидає його, замінює запасним гліфом, або піднімає помилку
Чому FPDFText_GetUnicode повертає значення, яке Character[] не може вмістити?
FPDFText_GetUnicode повертає LongWord, повне 32-бітне значення, бо кодування тексту PDF уже несе повне скалярне значення Unicode для кожного гліфа. CMap ToUnicode у PDF відображає коди символів на текст Unicode, і коли гліф представляє те, що неформально називають символом астральної площини — будь-що поза базовою багатомовною площиною, — це відображення є повною кодовою точкою, не 16-бітним фрагментом. PDFium декодує це назад у скалярне значення внутрішньо й повертає через межу DLL через FPDFText_GetUnicode, і саме на цій межі 32-бітне значення мусить стати чимось, що властивість Delphi може повернути вашому коду
Очевидна реалізація — WideChar(FPDFText_GetUnicode(TextPage, Index)), і вона теж неправильна. Жорстке приведення з 32-бітного значення до 16-бітного типу зберігає лише молодші 16 бітів і мовчки викидає решту, без жодного винятку й без перевірки діапазону. Для U+1F600 це означає збереження $F600 і втрату факту, що справжнє значення взагалі колись перевищувало U+FFFF, що виробляє одиницю коду, яка навіть не є дійсним висячим сурогатом, просто непов'язаним символом базової багатомовної площини, що випадково поділяє ці молодші біти. З'єднайте кілька тисяч таких у рядок, і подальший код уже не має способу відрізнити пошкоджений символ від легітимного
Що тепер повертають Character[] і Charcode[] для кодових точок астральної площини
Character[] та Charcode[] компонента PDFium повертають U+FFFD, символ заміни Unicode, щоразу коли базова кодова точка перевищує U+FFFF, замість того щоб мовчки її обрізати. Ця охорона сидить прямо всередині гетера властивості за Character[]
function TPdf.GetCharacter(Index: Integer): WideChar;
var
Code: LongWord;
begin
LoadTextPage;
Code := FPDFText_GetUnicode(FTextPage, Index);
if Code > $FFFF then
Result := #$FFFD // astral-plane code point: cannot fit in one WideChar
else
Result := WideChar(Code);
end;
Повернення U+FFFD замість обрізаного фрагмента — навмисне, вузьке виправлення, а не редизайн. Character[] та Charcode[] типізовані як WideChar і на TPdf, і на TPdfView, і розширення цього типу повернення для несення повної кодової точки зламало б кожного наявного викликача, що очікує один гліф на індекс, який означає одне 16-бітне значення. U+FFFD — власний призначений заповнювач Стандарту Unicode саме для цієї ситуації, тож викликач, що перевіряє на нього, отримує визначений, задокументований сигнал замість мовчки неправильних даних. Одна межова ситуація, яку варто знати: U+FFFD також легітимний символ сам по собі, тож на рідкісному документі, що вже містить справжній гліф символу заміни, цей індекс невідрізнимий від обрізаного астрального символу лише за значенням
Як правильно вилучити текст із емодзі та розширення B CJK у Delphi?
Викликайте Text замість обходу Character[], коли справжній текстовий вміст важить, бо Text читає через FPDFText_GetText і повертає повний WString із правильними сурогатними парами для кожного символу астральної площини в діапазоні, а не одне значення фіксованої ширини на індекс. Pdf.Text(0, MaxInt), чи скорочення Pdf.Text, вилучає всю сторінку правильно одним викликом, а Pdf.Text(StartIndex, Count) витягує менший діапазон так само. Character[] усе ще заслуговує на своє місце, коли вам потрібні лише позиція, шрифт чи дані прапорця за індексом, і ви ніколи не торкаєтеся самої кодової точки — CharacterOrigin[], FontSize[] та CharacterMapError[] не переймаються тим, чи базовий гліф був астральним
function ExtractLineSafely(Pdf: TPdf): WString;
var
I: Integer;
begin
Result := '';
for I := 0 to Pdf.CharacterCount - 1 do
if not (Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I]) then
Result := Result + Pdf.Text(I, 1); // full code point, never a truncated WideChar
end;
Перевірка «пропустити згенероване й невідображене» в цьому циклі — той самий шаблон, що використовується для звичайного вилучення тексту в статті вилучення тексту з документів PDF за допомогою компонента PDFium; єдина зміна — останній рядок, що обмінює прямий додаток Character[I] на виклик за одним індексом у Text, тож астральні символи прибувають як повні сурогатні пари замість заповнювачів заміни
Де це насправді кусає: експорти чатів, особисті імена та вбудовані шрифти CJK
Емодзі з'являються всюди, де PDF фіксує неформальне спілкування: експортовані журнали чатів, дампи відгуків з магазинів застосунків, стенограми з тікет-систем, збережені в PDF для архіву відповідності. Розширення B CJK з'являється у вужчому, але важливішому місці — особисті імена та назви місць, — бо японські сімейні реєстри, китайські записи реєстрації домогосподарств та тайванські документи посвідчення особи — класичні джерела символів, що ніколи не потрапили до звичайного блоку CJK. Конвеєр нарахування зарплати чи верифікації особи, що вилучає імена зі сканованих державних документів, — саме той тип навантаження, де мовчки спотворений символ перетворюється на невдалий збіг, а не на косметичну ваду
Рідкісні ідеографи CJK також мають тенденцію подорожувати з проблемами шрифту, а не лише кодування, бо шрифту доводиться нести гліф для кодової точки з діапазону U+20000, перш ніж узагалі щось зможе відрендеритися, а мало встановлених системних шрифтів це роблять. Будь-хто, хто вже обходить FontIsEmbedded[] для кожного символу так, як описує стаття читання властивостей шрифтів PDF за допомогою компонента PDFium, має перевіряти той самий індекс на обидві проблеми разом: індекс, що повертає U+FFFD з Character[] і повідомляє про невбудований шрифт, — документ, що не буде ні вилучений, ні надрукований правильно для цього символу, і виправлення належить вище за течією, у тому, як був створений PDF, а не у вашому коді вилучення
Властивості Character[], Charcode[] та Text, описані тут, — частина стандартного компонента PDFium для Delphi та C++Builder