HotPDF Delphi Component възстановява интервалите между думи и новите редове в THotPDF.ExtractLoadedPageText от глифна геометрия, не от символи за интервал. Интервал влиза, когато разстоянието след собствената ширина на глифа надхвърли 0.15 от височината на текста, а нов ред започва само когато началото на текста се премести срещу посоката на писане с повече от половината височина на текста. От v2.768.3 текстът на страницата включва и текст, рисуван през Form XObjects, и пропуска глифи извън видимата crop зона. Останалата част от статията обяснява защо всяко правило изглежда така, защото всяко от тях замени по-просто правило, даващо правдоподобен, но грешен изход върху реални документи
Симптомите са познати на всеки, който е пускал PDF текст в търсачески индекс. Корична страница се извлича като PDFReferenceManualNovember4,1998, данъчен бланк се разпада на 156 реда, диагонален watermark пристига по един символ на ред, а отрязано корицарско доказателство започва със slug реда на принтера, който никакъв viewer не показва. Нито един от тези файлове не е повреден. Всеки ползва съвършено легален начин за поставяне на текст, който наивен extractor чете погрешно
Защо извлеченият PDF текст губи интервалите си?
Извлеченият текст губи интервалите, защото от PDF никога не се изисква да ги съдържа. Производител може да разделя думи, като покаже символ интервал, но също толкова добре може да премести писалката с число вътре в TJ масив (ISO 32000-1 §9.4.3) или със свеж Td (§9.4.2), а TeX изходът, много Distiller файлове и повечето подравнени оформления правят точно това. Преди v2.766.76 HPDFAssemblePageText гледаше само вертикално движение, така че разбивка на дума, направена с позициониране, просто изчезваше. Асамбльорът вече мери, по посоката на писане на предишния глиф, разстоянието от края на собствената му ширина до началото на текущия глиф и вмъква един интервал, когато разстоянието надхвърли 0.15 от височината на кутията на текущия глиф, мерена от ascent до descent в user space. Не се добавя интервал, когато някоя от страните вече е празна, и нищо между два CJK символа, защото подравняването раздалечава идеографи без това раздалечаване да значи граница на дума. Глифните записи излагат същата геометрия, така че можете да възпроизведете решението, когато конкретен файл ви озадачи
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// височина на glyph кутията от ascent до descent, в user space
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// хоризонтален текст: разстояние от края на собствената ширина на предишния глиф
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
Защо да мериш от собствената ширина на глифа, а не от позицията на писалката?
HotPDF мери интервалите между думи от GlyphEndX / GlyphEndY, защото позицията на писалката след глиф вече съдържа разстояния, които не са празнота. ISO 32000-1 §9.4.4 дефинира хоризонталното отместване като ширина на глифа по размера на шрифта, плюс character spacing Tc, плюс word spacing Tw, всичко мащабирано с Tz. BaselineEndX / BaselineEndY държат цялото отместване, докато GlyphEndX / GlyphEndY държат само шрифтовия advance и Tz. Разликата има значение за производители, които стягат tracking с отрицателен Tc и после връщат разстоянието чрез TJ корекция след всеки глиф: мерено от позицията на писалката, връщането изглежда като празнота, а китайският израз “95后” се извличаше като “9 5 后”. Прагът е вързан за височината на glyph кутията, а не за размера Tf по подобна причина. Word експорти често записват 1 Tf и носят истинския размер в мащабиран Tm, така че Tfs казва 1, докато текстът е висок 10 пункта, а правило, ключовано на Tfs, би третирало двата изписвания на същата страница различно
Правилото има честни ръбове. Заглавие, набрано с много разхлабен tracking, при който Tc сам отваря повече от 0.15 от височината на текста между буквите, се извлича с интервал между всяка буква — така изглежда страницата, но вероятно не това искате да индексирате. Парчета, нарисувани извън ред на една базова линия, дават отрицателна празнота и се слепват без интервал. Нито единият, нито другият случай е чест в основния текст, а върху тестов корпус промяната повиши съвпаденията на думи срещу reference extractor на 28 страници, без да понии нито едно
Кога HotPDF започва нов ред в извлечения текст?
От v2.766.79 нов ред започва, когато преместването от началото на предишния глиф към текущия, проектирано върху нормалата на предишната посока на писане, надхвърли половината от по-голямата кутия височина на двата глифа. По-старото правило сравняваше суровото Y преместване с половин Tfs, което се проваляше в две посоки. С 1 Tf и мащабиран Tm прагът се свиваше до половин единица, така че горен индекс, вдигнат с text rise 0.4, или обикновено трептене на базовата линия чупеше реда. Правилото също игнорираше X изцяло, така че текст под обърнат Tm стъпваше надолу по страницата с всеки глиф и излизаше по един глиф на ред. Проектирането върху нормалата на посоката кара обърнатите пасажи да се държат като хоризонталните, а вземането на по-голямата от двете височини пази голяма пробна дума и малкият ѝ надпис на един ред, когато споделят базова линия. На споменатия данъчен бланк броят редове падна от 156 на 97. Вертикален текст в писмен режим 1 (§9.7.4.3) върви по отделен път: тези глифи се групират в колони, четат се отдясно наляво и отгоре надолу, с нов ред при всяка смяна на колоната
Кой текст ExtractLoadedPageText включва, а кой пропуска?
ExtractLoadedPageText връща текста, който viewer показва. От v2.766.80 работи само от видимите глифи, изхвърляйки всеки глиф, чийто център на кутията пада извън GetLoadedPageVisibleBox — CropBox, отрязан до MediaBox (§14.11.2). Това маха slug редове и други принтерски марки, набрани като текст извън областта за рязане. ExtractLoadedPageGlyphs нарочно продължава да връща всеки глиф на content stream-а на страницата, така че пак можете да намерите този материал, когато ви потрябва. Филтърът е тест за кутия, не тест за видимост: текст, скрит от clipping път, рисуван на бяло или покрит от изображение, пак се извлича
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// всеки глиф на page content stream-а, включително slug редът
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// само това, което страницата показва, с вплетен Form XObject текст
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
Текст, рисуван през Form XObjects, е част от текста на страницата от v2.768.3 насам. Хедъри, щампи и watermark-и много често живеят във форми, а някои стандартни документи губеха 30 до 35 процента от символите си преди промяната. THotPDF.InterpretContentWithForms записва всеки Do заедно с действащия CTM, интерпретира формата при неговия /Matrix по този CTM (§8.10.1) и вплита глифите на формата на позицията на Do, рекурсирайки във вложени форми. Форма без собствен /Resources назъмва тези на stream-а, който я рисува, както позволява §7.8.3. Form глифите носят TokenIndex = -1, а ExtractLoadedPageGlyphs и досега връща само глифи от page stream, защото търсенето, замяната и redaction записват промените обратно през TokenIndex и биха редактирали грешни байтове, ако form глиф се промъкне. Две опростявания си струват да се знаят: form текст не се отрязва до /BBox на формата, а рекурсията спира на 12 нива вместо чрез откриване на цикли, така че зле оформена форма, рисуваща себе си, повтаря текста си, докато стигне тази таван
Защо текст след оператор Q се декодираше като глупости?
Текст след Q можеше да се декодира погрешно преди v2.766.73, защото extractor-ът пазеше само CTM на q. Параметрите на текстовото състояние — шрифт, размер, Tc, Tw, Tz, TL, режим на рендиране и rise — принадлежат на graphics state (§9.3.1), така че Q трябва да ги възстанови заедно с всичко друго на стека (§8.4.2). Един индустриален доклад избираше двубайтов Identity-H шрифт вътре в q … Q и после показваше еднобайтов WinAnsi текст без собствен Tf. Extractor-ът държеше вътрешния шрифт, четеше водещите точки и думата “Adobe” в съдържанието като двубайтови кодове и изпускаше 15% от символите на страницата. Стекът q/Q на интерпретатора вече държи пълното текстово състояние. Правилата за извличане, описани тук, важат за всяка страница, така че целият документ може да отиде във файл с едно извикване
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// празен диапазон = всички страници; form feed между страниците; UTF-8 BOM
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
Кой HotPDF текстов API да ползвате?
ExtractLoadedPageText остава в реда на content stream, което е правилното подразбиране за търсене и индексиране; декодиращата верига под него е покрита в извличането на текст от заредени PDF-и с HotPDF. За tagged документи, при които редът на авторство има значение, извличането на текст в структурен ред минава структурното дърво, вместо да отгатва от геометрия, а за дана, заклещена в таблици, типизираното извличане на таблици през прехвърляния на страници връща клетки, а не редове. Пълната API референция и trial изтегляне са на продуктовата страница HotPDF Delphi PDF Component