HotPDF превръща сканирани PDF страници в търсим PDF с Tesseract чрез HPDFCreateTesseractOCREngine — фабрика, която опакова локално инсталиран Tesseract изпълним файл като IHPDFOCREngine. Подавате този engine на ApplyLoadedOCRTextLayer, който рендира всяка страница, пуска Tesseract по веднъж на страница, парсва TSV изхода му на ниво дума и ангажира невидим Unicode текстов слой за всички поискани страници в една транзакция, или за нито една
Причината този адаптер да съществува е обхватът. Вграденият OCR двигател с template matching е нарочно тесен: машинно печатни ASCII букви и цифри, нищо друго. Фактури с имена с диакритика, китайски договори и многоезични архиви се нуждаят от истински разпознавател с обучени езикови модели, а Tesseract е очевидният кандидат, защото е командна програма, която можете да provision-нете до приложението си. Да викаш външна програма от документна библиотека звучи тривиално. Не е, и по-голямата част от интересния код в адаптера е за това какво става, когато програмата се държи зле, виси, бива отменена или наследи неща, които никога не трябва да вижда
Как HotPDF управлява Tesseract от Delphi приложение?
HotPDF пуска Tesseract като скрит child процес на страница, хранейки го с рендиран битмап и четейки обратно TSV файл, и излага резултата през същия IHPDFOCREngine шев, който вграденият engine ползва. Нищо надолу не се мени: координатното мапване, обработката на завъртане, Unicode валидацията, филтрирането по confidence и атомарното ангажиране са text-layer линията, която вече имате. Фабриката живее в unit-а HPDFTesseractRecognition и валидира рано: изпълнимият файл трябва да съществува, tessdata директорията трябва да съществува, timeout-ът трябва да е между 1 и 3 600 000 милисекунди, а идентификаторът на езика може да съдържа само ASCII букви, цифри, _ и +. Последната проверка има значение, защото езиковият низ попада на команден ред, а eng+chi_sim е легитимна Tesseract стойност, докато каквото и да е с кавички или интервали — не е
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// вдига EArgumentException за липсващ изпълним файл, липсваща tessdata,
// зле езиков идентификатор или timeout извън 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // няколко модела, съединени с '+'
120000); // лимит на страница, по подразбиране 60000
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Options := THPDFOCRTextLayerOptions.Default; // 300 DPI, MinimumConfidence 0.5
Options.CancellationToken := Token;
// празен списък страници значи всяка страница; страници с текст се прескачат по подразбиране
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
begin
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
end
else
case Info.Status of
otlsCancelled: Writeln('Cancelled, document unchanged');
otlsEngineError: Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
finally
Doc.Free;
end;
end;
За всяка страница Recognize създава лична директория под temp пътя, на име HotPDF-OCR-{GUID}, записва рендирания битмап като input.bmp и пуска tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, като всеки path аргумент е в кавички по правилата на Windows за екраниране на backslashes и вградени кавички в команден ред. Стойността --dpi е render DPI от THPDFOCRTextLayerOptions.DPI, така че Tesseract никога не трябва да гадае резолюцията от image metadata, а --psm 3 иска напълно автоматична сегментация на страницата. Engine-ът се представя като Tesseract (local CLI), което е това, което отива в Info.EngineName. Tesseract и езиковите му модели не идват в пакета на HotPDF; инсталирането им е работа на приложението
Защо TSV parser-ът е толкова строг?
TSV parser-ът в HotPDF проваля цялата страница при всеки зле оформен ред, защото частично парснат списък от думи ражда текстов слой, който тихо се разминава с изображението. TSV изходът на Tesseract има фиксиран header от дванайсет колони, от level до text, а HotPDF сравнява първия ред с точно този header, след като махне опционален byte order mark. Всеки следващ ред трябва да се раздели на точно дванайсет полета, а разделянето спира след единайсетия таб, така че таб вътре в разпознатия текст си остава част от думата, вместо да създаде тринайсета колона. Само редовете на ниво 5 са думи; нивата 1 до 4 описват страници, блокове, параграфи и редове и се прескачат. Редове на ниво 5 с празен или чисто whitespace текст също се прескачат, защото празна дума има кутия, но нищо за локализиране или търсене. Всичко останало се проверява здраво: целочислена геометрия, confidence, парснат с инвариантен en-US формат, така че немски locale да не прочете 93.5 като боклук, кутия, лежаща изцяло вътре в битмапа, и confidence между 0 и 100. Един-единствен провал вдига грешка, engine-ът връща False, а масивът с думи се изчиства. Регресионите тестове включват точно този случай: една валидна дума, следвана от счупен ред, трябва да даде нула думи, не една
// съкратено от level-5 цикъла в HPDFLocalTSVRecognition
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue; // редове страница/блок/параграф/ред
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // whitespace думи нямат позиция
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
not TryStrToFloat(Fields[10], Confidence, Settings) then
raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
(Int64(X) + W > Request.Bitmap.Width) or
(Int64(Y) + H > Request.Bitmap.Height) or
not ((Confidence >= 0) and (Confidence <= 100)) then
raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100; // линията очаква 0..1
Последният ред взаимодейства с подразбиране, което може да не очаквате. Confidence на Tesseract върви от 0 до 100, линията работи в 0 до 1, а THPDFOCRTextLayerOptions.MinimumConfidence подразбира 0.5, така че всяка Tesseract дума под 50 се брои в Info.DroppedWordCount и никога не стига страницата. На чист сканиране при 300 DPI това е разумен праг. На шумен факс може да хвърли изненадващ дял от страницата, а правилният ход е да погледнете отпадналото число, преди да свалите прага, защото думите с ниско confidence са точно онези, които най-вероятно са грешни
Какво наследява Tesseract child процесът?
Tesseract child процесът наследява точно два handle-а от HotPDF: NUL handle за standard input и output и файлов handle за standard error. Тази прецизност е самият смисъл. CreateProcess с bInheritHandles = True е начинът да подадете стандартни handle-и на дете, но сам по себе си той подава всеки наследяем handle в host процеса — включително файлове, pipe-ове и event-и, отворени от несвързан код във вашето приложение. Детето после пази тези обекти живи, докато излезе, така че файл остава заключен или pipe никога не вижда края си, докато Tesseract меле страница. HotPDF затваря тази дупка с разширен startup запис: STARTUPINFOEX, списък с атрибути, носещ PROC_THREAD_ATTRIBUTE_HANDLE_LIST, и creation флага EXTENDED_STARTUPINFO_PRESENT. С handle списъка на място bInheritHandles пак трябва да е True, но само изброените handle-и минават границата. Същото мислене за ограждане движе изолирането на PDF image кодеци в worker процеси, където детето е код, на който не се вярва; тук на детето се вярва, но host-ът не е единственият собственик на собствената си handle таблица
// константите са показани по име; източникът подава числовите им стойности
// и двата handle-а са създадени с bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin и stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt в личната директория
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
@InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
True, // изисквано от handle списъка
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
Защо отменено OCR изпълнение може да изглежда като engine провал?
Отменено OCR изпълнение изглежда като engine провал, защото IHPDFOCREngine.Recognize връща един-единствен Boolean, а False значи и „Tesseract се провали", и „потребителят натисна Cancel". Адаптерът pollва token-а за отмяна и timeout-а на всеки 25 милисекунди, докато детето работи, и когато token-ът се включи, вдига изключение вътре в Recognize, хваща собственото си изключение, чисти след себе си и връща False с диагностика. Ако линията прие това за engine грешка, извикващият щеше да види otlsEngineError за работа, която потребителят нарочно е спрял. ApplyLoadedOCRTextLayer затова проверява token-а първо, всеки път когато Recognize върне False, и превръща резултата в engine провал само ако token-ът не е бил вдигнат. Този ред пази многостраничния договор: разпознаване, валидация, budget сметка и строене на съдържание вървят за всяка поискана страница, преди транзакцията върху графика да се отвори, така че отмяна на страница 40 от 50 докладва otlsCancelled и оставя документа — включително първите 39 страници — недокоснат. Няма частично търсим файл, който после да обяснявате, а останалата обработка на провали следва същия ограничен стил:
- Timeout-ът е на всяко извикване на
Recognize, мерен от старта му, така че подразбираните 60 000 ms важат за всяка страница, а не за целия документ - Детето, което още работи при timeout или отмяна, се terminate-ва, чака се до 5 секунди, а личната му директория се изтрива в
finallyблок output.tsvе капиран на 64 MiB, аstderr.txtна 1 MiB, проверявани докато детето работи и след като излезе- Броят думи и UTF-16 code единици се капират на страница от оставащите бюджети
MaxWordsPerPage,MaxTotalWordsиMaxTextCodeUnits, а надминаването им проваля изпълнението, вместо да съкращава списъка с думи - Standard output отива на
NUL, защото Tesseract пишеoutput.tsv, докато standard error отива във файл, така че ненулев exit код се докладва с до 4 096 знака от собствения оплак на engine-а — обикновено най-бързият начин да научите, че.traineddataфайл липсва
Как разпознатите думи стават невидим текстов слой
HotPDF записва Tesseract думите като невидим текст с text rendering режим 3 — режима „нито fill, нито stroke" от ISO 32000-1 §9.3.6, така че страницата и нататък показва сканираното изображение, докато търсенето и копирането работят върху разпознатите думи. Content stream-ът отваря BT с 3 Tr, а всяка дума получава Tm матрица на своя baseline, размер на шрифта, извлечен от височината на кутията в пиксели при render DPI, и Tz хоризонтален мащаб, опъващ glyph изпълнението до измерената ширина на кутията — затова подчертаване при търсене попада върху думата в изображението, вместо да се лута из нея
TSV-ът на Tesseract има кутии, но няма baseline-и, така че адаптерът докладва всяка дума без такъв, а линията оценява baseline на една пета от височината на кутията над долния ѝ ръб. Самият текст минава през споделен unembedded Type0 шрифт с Identity-H кодиране и генерирана ToUnicode CMap — по един CID за всеки различен Unicode scalar в цялото изпълнение — затова китайски, латиница с диакритика и знаци от supplementary plane-а оцеляват при копиране и търсене. Този дизайн има две граници, които си струва да се кажат отрано: едно изпълнение може да носи най-много 65 535 различни scalars, а unembedded шрифтът не удовлетворява изискването за вграждане на шрифтове на ISO 19005, така че PDF/A изход се нуждае от отделно вграден конформен шрифт. Проверката на резултата е проста и си струва автоматизация: запишете, заредете пак и пуснете обикновения текстов път за зареден документ от извличането на текст от зареден PDF в Delphi; ако думите се върнат на очакваните страници, слоят е истински
RapidOCR и други двигатели на същия TSV протокол
HotPDF преизползва същия process runner и TSV parser за RapidOCR чрез HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), което е по-полезният избор за сканирания на опростен китайски. Командният ред е идентичен, освен че пътят на bridge скрипта се вмъква след Python изпълнимия файл, а езикът е фиксиран на chi_sim. HotPDF доставя bridge-а като tools/OCR/rapidocr_tsv.py; той очаква пакетите rapidocr и onnxruntime плюс три локални ONNX модела, изключва автоматичните изтегляния на модели и пише TSV във формата на Tesseract, така че Delphi страната не се нуждае от втори parser. Името на engine-а, докладвано в Info.EngineName, е RapidOCR (local ONNX). Тази форма подсказва общата рецепта: всеки разпознавател, който можете да опаковате в малък скрипт, приемащ списъка с аргументи в стил Tesseract и излъчващ дванайсетколонния TSV, наследява изолацията на handle-и, timeout-а, отмяната, бюджетите на изхода и ангажирането all-or-nothing безплатно. Адаптерите са само за Windows, вървят по една страница наведнъж синхронно и не изправят наклонено или преобрадват изображението отвъд това, което рендерът дава, така че качеството на изображението на входа и нататък поставя тавана на изхода
Tesseract и RapidOCR адаптерите, writer-ът за невидим текстов слой, page рендерът, който ги храни, и извличането на текст, което проверява резултата, излизат всичките в същия нативен VCL компонент за Delphi и C++Builder. Ако добавяте OCR към приложение за документно прихващане или архивиране, HotPDF Delphi PDF компонента ви дава линията, като остава само самият OCR engine за инсталиране