首页 / 资讯中心 / 文章详情

Windows 下 Tesseract 构建:ICU 预编译包接入与排错

Windows 下 Tesseract 构建:ICU 预编译包接入与排错 ★ FEATURED ARTICLE
简介icu4c-56_1-Win64-msvc10.zip 是面向 Windows 64 位、MSVC10 编译环境的 ICU 库分发包服务于在 Tesseract OCR 或 C 工程中引入 Unicode 与国际化能力的开发者。包内共 220 个文件、约 12.9MB以 182 个头文件为主另有 15 个可执行程序、8 个动态库及 lib/exp 链接文件覆盖接口声明、运行工具、运行时组件与编译链接需求并附许可说明便于核对授权。已有 599 人学习对旧版 Visual Studio 工程和多语言 OCR 构建仍有参考价值。读者可据此配置头文件与库路径、在 CMake 中定位依赖并排查链接问题也可借助工具验证 Unicode 处理流程适合复现 Tesseract 多语言识别环境、理解 ICU 组成及旧工具链集成方式。1. 为什么 Tesseract 的 Windows 构建老卡在一个 ICU 压缩包上去年帮某实验室搭一套 Tesseract 的 Windows 构建环境源码、Leptonica、训练工具都齐了卡在最后一个依赖上——CMake 报Could NOT find ICU后面跟着一串include和library都找不到的提示。翻了下工程配置才发现Tesseract 的CMakeLists.txt里对 ICU 是可选依赖但少了它一些多语言字符串处理和 Unicode 规范化路径就会走不通最后编出来的东西不一定能用。这就是icu4c-56_1-Win64-msvc10.zip这类预编译包存在的意义它把 ICU 的库、头文件、数据文件打包好省去自己拉源码再配一遍编译链的麻烦。这套东西适合两类人一是要在 Windows 上用 CMake 编 Tesseract 却没耐心折腾 ICU 源码构建的二是接手了老工程、发现依赖清单里钉死 56.1 这个版本、需要一份能直接对上的二进制包。它不是 Python 那种pip install就能搞定的东西解压路径、库名、运行时 DLL 位置都有讲究往下几章把这块拆开。2. 解压后的目录结构CMake 到底想找哪些文件拿到压缩包第一步不是急着解压到工程目录里而是先看清楚里面长什么样。ICU 的 Windows 预编译包结构在不同版本里有过调整56.x 这一代常见的布局是bin64/、lib64/、include/三个目录也可能按bin/、lib/平铺取决于发布方打包时的习惯。2.1 先弄清壳子里的三件套不管目录怎么摆真正参与编译链接的东西就三样类别典型文件作用头文件unicode/utypes.h、unicode/ucnv.h、unicode/unistr.h等供 C/C 源码 include导入库icuuc.lib、icuin.lib、icudt.libMSVC 链接时用来解析符号运行时 DLLicuuc56.dll、icuin56.dll、icudt56.dll程序启动时加载跑起来才需要icuuc是公共核心库icuin是国际化组件库icudt是数据文件缺一个都可能让链接或运行崩在某个符号上。我在某次排查里就见过只拷了icuuc.lib的情况链接过程没报错一运行到UnicodeString拼接就挂原因是icuin的符号找不到、icudt的数据没加载。注意icudt在 56.x 时代既可能是.lib也可能是.dat数据文件Windows 的二进制分发通常做成icudt56.dll加一个占位.lib。如果解压出来只看到icudt56.dll没有对应.lib那说明这份包只打算在运行时用CMake 那边就不该把icudt当作必须的链接库去要求。先用系统自带的命令确认一遍# 假设解压到 D:/deps/icu4c-56_1 dir /s /b D:\deps\icu4c-56_1 | findstr /i \.lib$ \.dll$ \.h$这一步的目的不是记下所有文件名而是确认三个关键点unicode/目录的父级在哪里、icuuc.lib之类的库文件挂在哪个目录下、DLL 是否和.lib同属一个位宽分支bin64/对lib64/。这三件事决定了下面 CMake 参数怎么填。2.2 find_package(ICU) 的查找顺序和手动覆盖Tesseract 的 CMake 里通常会写find_package(ICU REQUIRED COMPONENTS uc i18n)CMake 自带的FindICU模块会按一套顺序去找先看ICU_ROOT缓存变量再看CMAKE_PREFIX_PATH里拼出来的路径最后扫系统常见目录。这里有两个容易踩的点第一ICU_ROOT不是所有 CMake 版本都认的通用变量。CMake 3.7 之后FindICU才开始稳定支持ICU_ROOT再往前的老版本要么不识别要么识别了但不去拼include/和lib/子目录。所以填之前先看一眼工程用的 CMake 版本用cmake --version一查就知道。版本偏低时更稳妥的方式是手动指定ICU_INCLUDE_DIR和ICU_LIBRARY绕开模块的路径猜测逻辑。第二FindICU找的是「位宽匹配」的库。你在 64 位工程里让它去lib/下找它会正儿八经地报一堆“找到但版本不匹配”的日志最后ICU_FOUND为假看着像没找到实际上是找错了地方。这种情况直接set(ICU_ROOT D:/deps/icu4c-56_1)不行得把根路径指到具体那层比如D:/deps/icu4c-56_1/lib64和对应的include分别给。一个比较稳的手动覆盖写法是这样# 显式给出 ICU 的头文件和库路径绕开 FindICU 的自动猜测 set(ICU_INCLUDE_DIR D:/deps/icu4c-56_1/include CACHE PATH ICU headers) set(ICU_UC_LIBRARY D:/deps/icu4c-56_1/lib64/icuuc.lib CACHE FILEPATH ICU uc lib) set(ICU_I18N_LIBRARY D:/deps/icu4c-56_1/lib64/icuin.lib CACHE FILEPATH ICU i18n lib) find_package(ICU REQUIRED COMPONENTS uc i18n) if (NOT ICU_FOUND) message(FATAL_ERROR ICU 没找对检查 ICU_ROOT/ICU_INCLUDE_DIR 是否指到了实际目录) endif()这段的核心是「先给缓存变量再让find_package去验证」。缓存变量一旦设上FindICU就直接读取不再自己扫。如果ICU_FOUND还是假说明连这个指定路径下的.lib都匹配不上例如 CMake 期待icuuc.lib但目录里只有icuucd.lib这时候就要去看解压出来的库名到底带不带d后缀。提示不要为了省事把 ICU 的bin64/直接塞进系统PATH。构建期会被 CMake 扫到但项目迁移到另一台机器上又得重新配等于把隐式依赖藏进了环境变量里。2.3 让 Tesseract 的 CMakeLists 认识这份 ICUTesseract 源码树里的CMakeLists.txt一般不会直接写死路径它只负责在find_package(ICU)成功后把ICU_INCLUDE_DIRS和ICU_LIBRARIES拼到target_include_directories和target_link_libraries里。也就是说你要做的不是改 Tesseract 的源码而是在 CMake 配置阶段通过命令行把路径喂进去。命令行传参的正确姿势是这样的cmake -S . -B build ^ -G Visual Studio 14 2015 Win64 ^ -DICU_ROOTD:/deps/icu4c-56_1 ^ -DICU_INCLUDE_DIRD:/deps/icu4c-56_1/include ^ -DICU_UC_LIBRARYD:/deps/icu4c-56_1/lib64/icuuc.lib ^ -DICU_I18N_LIBRARYD:/deps/icu4c-56_1/lib64/icuin.lib ^ -DCMAKE_BUILD_TYPERelease注意这里的生成器写的是 VS2015不是压缩包名里的msvc10。这中间的对应关系是下一章的重点暂时按下。先把「路径对不对」这条支线走通配置跑完回头看build/CMakeCache.txt搜ICU_开头的项ICU_FOUND:BOOL1出现说明找对了还剩ICU_FOUND:BOOL0就往上翻配置日志里「Could NOT find ICU」后面那段它会明确写着「found version 56.1, but the requested version is …」或者「icuuc.libnot found in …」之类的线索。这一步做完说明工程知道 ICU 在哪了还谈不上链接成功。下一章进入真正的编译闭环。3. Tesseract 工程里的 ICU 接入从 CMake 配置到链接通过3.1 一次完整的 CMake 配置与链接命令把上一章的路径参数放进一个完整可跑的流程里顺序通常是先配ICU_ROOT再确认 Tesseract 用到的头文件和库都从这份包出最后让链接器抓到正确的.lib。set ICU_DIRD:\deps\icu4c-56_1 cmake -S tesseract-src -B tesseract-build ^ -G Visual Studio 14 2015 Win64 ^ -DCMAKE_BUILD_TYPERelease ^ -DICU_ROOT%ICU_DIR% ^ -DICU_INCLUDE_DIR%ICU_DIR%\include ^ -DICU_UC_LIBRARY%ICU_DIR%\lib64\icuuc.lib ^ -DICU_I18N_LIBRARY%ICU_DIR%\lib64\icuin.lib ^ -DLeptonica_DIRD:\deps\leptonica\cmake ^ -DBUILD_TRAINING_TOOLSOFF cmake --build tesseract-build --config Release --parallel 8三处参数值得单独拎出来讲。-G的生成器决定了后面用的是哪套工具链和哪个 CRT和msvc10这个命名能不能对上第 4 章会专门拆。-DBUILD_TRAINING_TOOLSOFF是因为训练工具会额外拉更多依赖初次打通链路时关掉能减少变量。-DLeptonica_DIR是指向 Leptonica 的cmake配置文件所在目录Tesseract 依赖它但很多教程只提 ICU 不提 Leptonica导致find_package(Leptonica)失败后有人误以为是 ICU 的问题。构建跑完后链接阶段正常应该没有任何unresolved external symbol。如果链接报了__imp_icu_…开头的符号找不到很典型的原因是链接时给的.lib和编译时声明用的头文件不同源——比如头文件是 56.1链接时被别的路径下 60.x 的icuuc.lib抢先命中。这种情况打开tesseract-build/CMakeFiles/tesseract.dir/link.txt或者工程属性里的 Linker → Input看实际参与链接的库列表路径一眼就能看到。3.2 写一个最小验证程序先确认 ICU 本身能用直接上 Tesseract 一编译就是几十分钟中间挂了还得从头查。更省时间的做法是先编一个十几行的 C 小程序只调 ICU 的几个基础接口确认「头文件 库 DLL」这条链路是通的。// icu_smoke.cpp #include unicode/utypes.h #include unicode/unistr.h #include unicode/ucnv.h #include cstdio int main() { UErrorCode status U_ZERO_ERROR; // 1. 打版本号确认运行时加载的 ICU 和头文件版本一致 UVersionInfo ver; u_getVersion(ver); std::printf(ICU version: %d.%d.%d.%d\n, ver[0], ver[1], ver[2], ver[3]); // 2. 做一次编码探测走 icuuc 的核心路径 UConverter* conv ucnv_open(UTF-8, status); if (U_FAILURE(status)) { std::printf(ucnv_open failed: %s\n, u_errorName(status)); return 1; } std::printf(converter name: %s\n, ucnv_getName(conv, status)); ucnv_close(conv); // 3. 触发 icuin 里的 UnicodeString让链接器真的用上 icuin icu::UnicodeString s icu::UnicodeString::fromUTF8(中文测试); std::printf(length: %d\n, s.length()); return 0; }编译命令里-I指向%ICU_DIR%\include-L/链接那把icuuc.lib和icuin.lib都塞进去cl /std:c14 /EHsc /MD ^ /I%ICU_DIR%\include ^ icu_smoke.cpp ^ /link /LIBPATH:%ICU_DIR%\lib64 icuuc.lib icuin.lib ^ /OUT:icu_smoke.exe几个参数的意图/MD是让程序用运行时库的 DLL 版本ICU 的官方 Windows 二进制通常也是按 DLL 运行时构建的两者混一起就会出「同一个堆上申请、另一个堆上释放」的经典崩/EHsc是 C 异常处理ICU 的 C 接口会抛icu::ErrorCode少了它可能编译期没事、运行期行为不对。如果这里的cl拿去编时报LNK2019找不到__imp_ucnv_open那就说明链接器根本没把icuuc.lib读进去回去核对/LIBPATH和库名。程序编出来跑ICU version: 56.1.0.0这行对上、中文length打出来是 4这条链路才算闭合。跑起来弹「找不到 icuuc56.dll」属于运行时缺 DLL把%ICU_DIR%\bin64下的三个 DLL 拷到 exe 同目录就能过——别急着往系统目录拷。3.3 运行时 DLL 到底放哪编译通过、链接通过不代表程序能跑。ICU 是运行时动态加载的Windows 的 DLL 搜索顺序是先 exe 所在目录、再System32、再PATH里的目录。三条路里最稳的是「exe 同目录」也就是把icuuc56.dll、icuin56.dll、icudt56.dll拷到编出来的tesseract.exe旁边。icudt56.dll尤其容易被漏。少了它程序启动能过一旦构造icu::UnicodeString或者调到需要 locale 数据的接口就会在数据文件加载那一步抛U_MISSING_RESOURCE_ERROR报错信息里通常带「icudt56l.dat」或者「icudt56.dll」的字样。我在某次部署里就只拷了icuuc和icuin两个测了英文没问题一上中文就崩查了两天才发现是数据文件缺位。血泪经验就是三个一起拷别省。需要检查运行时到底加载了哪个 ICU 时可以用系统资源监视工具看进程的模块列表也可以在代码里直接打印const char* dataDir u_getDataDirectory(); std::printf(ICU data directory: %s\n, dataDir);u_getDataDirectory()返回空串说明数据走的是打包在 DLL 里的形式也就是依赖icudt56.dll返回一个实际路径说明程序在从那个目录读数据文件路径对吗不上运行时就挂。这一条在排查「同一套代码A 机器能跑 B 机器不能跑」的场景里特别有用。4. 版本和工具链的边界msvc10 后缀到底意味着什么4.1 库名后缀、CRT 与 ABI 的对应关系压缩包名icu4c-56_1-Win64-msvc10拆开看是四层信息icu4c表示这是 ICU 的 C/C 实现集56_1是版本号Win64是 64 位 Windows 二进制msvc10表示用 MSVC 10.0 这套编译器也就是 Visual Studio 2010编出来的。这个后缀不是随手写的它决定了这份 ICU 二进制依赖哪一套 C 运行时。MSVC 2010 对应的是msvcr100.dll/msvcp100.dllMSVC 2015 及以后统一成 UCRT 体系msvcr140.dll起步。两套运行时把内存分配器的边界画得很清楚跨边界传std::string、icu::UnicodeString这类带堆内存的对象时一边申请一边释放就会在另一套运行时里炸。这也是为什么拿msvc10的 ICU 去配 VS2015 的工程编译和链接都可能顺利但一跑起来就在字符串操作上出问题。反推也能成立如果你的 Tesseract 工程是用 VS2010 建的那么这份 ICU 二进制就是完全对得上的选择msvcr100全家桶一致不需要做任何 ABI 适配。4.2 用 VS2015 编 Tesseract能不能直接用这份 ICU答案分两面。编译期能不能过取决于头文件声明和你工程里编译器的 ABI 兼容程度。C 接口部分ucnv_*、u_str*系列基本是稳定的头文件直接用问题不大C 接口部分icu::UnicodeString、icu::Locale就不一样了56.x 的头文件里已经开始涉及std::string和异常类型和 VS2015 的 STL 混用时只要编译参数和运行时库不一致就有链接或运行风险。一个相对稳妥的路线是C 接口全用C 接口尽量只当薄封装。Tesseract 本身对 ICU 的调用集中在 Unicode 字符串规范化、语言标签这些地方量不大真遇到问题改成 C 接口调用是最省事的。下表可以对着比一下工程编译器这份 ICU 的命令行可以直接用需要改的建议VS2010msvc10是无VS2015msvc14编译可能过链接需要/MD对齐优先 C 接口C 接口单独测VS2017/2019/2022不建议直接链接用源码编一套或找对应版本的预编译包这里面真正需要注意的是/MD和/MT的对齐。ICU 这份包是用 DLL 版 CRT 编的你的工程如果开了/MT静态 CRT混进来之后malloc和free落到两个堆上运行期出错是迟早的事。看工程属性里 C/C → Code Generation → Runtime Library 那一栏Release 下应该是/MDDebug 下是/MDd。4.3 什么时候该放弃这份二进制包改源码编情况有三类。一类是编译器代差太大比如 VS2019 拿这份包当主依赖与其做兼容性试验不如直接拉 ICU 源码用同一套工具链编一遍。另一类是工程里已经有其他依赖比如某个库也在用 ICU版本必须统一靠换预编译包是换不掉版本冲突的。第三类是要静态链接icu4c的预编译包通常是动态库形式静态版本需要自己加-DU_STATIC_IMPLEMENTATION编预编译包满足不了。源码编的路子大概是git clone之后用 CMake 走一遍关键是版本要和工程其他部分对上。这条路走得慢但可控适合对「是哪份库、用哪个 CRT」这条链路要求完全确定的场景。对这种场景我一般会先把预编译包跑通一遍验证代码再把同一个版本号的源码编出来替换进去对比两次的icu_smoke.exe打出来的版本号一致、行为一致再合入主工程。5. 避坑排查这几条踩过的坑踩一次记一辈子5.1 配置阶段的怪现象现象CMake 日志里写着Found ICU: ... (found suitable version 56.1)但紧接着又Could NOT find ICU。原因工程里有两次find_package(ICU)第一次找到了第二次被某个子目录里的add_subdirectory重新执行了一次而那个子目录的CMAKE_MODULE_PATH里带了一份自定义的FindICU.cmake把之前找到的结果覆盖成了空。 解决去build/CMakeCache.txt搜ICU_开头的所有项看有没有被INTERNAL标记的残留项或者把自定义的FindICU.cmake重命名让 CMake 用系统自带的那个版本重配一遍。现象ICU_FOUND为真ICU_INCLUDE_DIR也指对了但编译 Tesseract 的unicode/…那一堆头文件时报cannot open include file。原因ICU_INCLUDE_DIR指的是include根而 C 源码里 include 的是unicode/utypes.h。如果那个目录下还有一层unicode/unicode/…的多余嵌套路径就错位了。 解决用dir /b列一下ICU_INCLUDE_DIR目录下有没有unicode子目录没有就说明根指错了往上或往下挪一层。5.2 链接与运行时的坑现象链接报了十几行LNK2005 error符号名看着都像icu_56::…。原因.lib进了两次。一次是从ICU_LIBRARIES自动带入一次是工程里手写了target_link_libraries(... icuuc.lib ...)同一个库被链接器读了两遍MSVC 就报重复符号。 解决target_link_libraries里统一用 CMake 给的ICU_LIBRARIES变量别手写库名或者检查link.txt里同一个路径出现几次改成用INTERFACE依赖。现象Tesseract 编出来的 exe 双击闪一下就退出命令行跑也不报错日志停在 Unicode 初始化那一步。原因icudt56.dll没在 exe 同目录或者放进去的是 32 位版本进程启动时加载失败但 Windows 的默认加载器不弹「找不到 DLL」对话框默默退出。 解决资源管理器里看进程模块列表或用依赖查看工具查icuuc56.dll的下游依赖同时用dumpbin /headers icudt56.dll看 Machine 字段x64 下应该是0x8664不是0x14c。现象单测、调试工具里一切正常部署到另一台机器上中文处理挂了报U_INVALID_CHAR_FOUND。原因那台机器上PATH里另一套软件的目录里有老版本 ICU 的 DLL比如 4.xWindows 先加载了它u_getVersion返回的还是老版本你的程序按 56 的头文件去调 4.x 的接口行为就对不上了。 解决部署包里把三个 DLL 放 exe 同目录Windows 的加载顺序里 exe 目录优先级最高能压住PATH里的同名 DLL不要指望改PATH顺序。5.3 一次跟着日志走的排查路径真遇到icu_smoke.exe跑不起来的场景按这个顺序走能少绕弯先dumpbin /dependents icu_smoke.exe看它到底在找哪个 DLL再dumpbin /headers看 DLL 的位宽和 CRT 依赖最后用 Process Monitor 抓进程启动时的文件加载路径看它在哪一步失败。这条路径慢一点但每一步都有实证比一根筋去猜「是不是库没找对」要靠谱得多。6. 把这份 ICU 用顺手的几个技巧先说一个我踩过的坑。某次为了省事直接把%ICU_DIR%\bin64加进了系统PATH然后拿另一台机器复现构建怎么都编不过查了两小时才发现那台机器的PATH里有一个同名但版本不同的目录抢先命中。从那以后我每次配 ICU 都强制走一遍「显式ICU_ROOT 缓存变量 icu_smoke验证」的三步坚决不靠环境变量吃饭。下面这个流程可以贴到脚本里复用:: verify_icu.bat echo off setlocal if %ICU_DIR% ( echo [FAIL] ICU_DIR 未设置 exit /b 1 ) :: 1. 三个关键文件是否齐 if not exist %ICU_DIR%\include\unicode\utypes.h ( echo [FAIL] 缺少头文件 exit /b 1 ) if not exist %ICU_DIR%\lib64\icuuc.lib ( echo [FAIL] 缺少 icuuc.lib exit /b 1 ) if not exist %ICU_DIR%\bin64\icuuc56.dll ( echo [FAIL] 缺少 icuuc56.dll exit /b 1 ) :: 2. 用 cl 编一遍 smoke能过就说明工具链和这份 ICU 对得上 cl /nologo /std:c14 /EHsc /MD /I%ICU_DIR%\include ^ icu_smoke.cpp ^ /link /LIBPATH:%ICU_DIR%\lib64 icuuc.lib icuin.lib ^ /OUT:icu_smoke.exe if errorlevel 1 ( echo [FAIL] 编译链接失败看上面的 LNK 报错 exit /b 1 ) :: 3. 把 DLL 拷到旁边再跑避免 PATH 污染 copy /Y %ICU_DIR%\bin64\icuuc56.dll . nul copy /Y %ICU_DIR%\bin64\icuin56.dll . nul copy /Y %ICU_DIR%\bin64\icudt56.dll . nul icu_smoke.exe if errorlevel 1 ( echo [FAIL] 运行失败核对 DLL 版本和位宽 exit /b 1 ) echo [OK] ICU 链路可用 endlocal脚本里三条判断的分工很明确第一条守住「头文件在哪」第二条守住「链接库在哪」第三条守住「运行时 DLL 在哪」三条全过再进编译。用一个批处理把这三步固定下来比每次重新回忆参数名要快得多。再补一个版本确认的小技巧。ICU 的 C 接口里u_getVersion打出来的是 56.1.0.0 这种形式C 接口里icu::UnicodeString的静态成员getStaticClassID可以用来触发动态加载链如果它返回了非零值说明icuin那份 DLL 确实加载进来了不是只连了icuuc。在部署后自检里把这两个都打印出来能省掉「到底哪个 DLL 没加载」的猜谜过程。提示如果工程里同时对 ICU 和 Tesseract 的多线程版本有要求编译参数里最好统一/MT或/MD并且把 ICU 的 DLL 版本和 Tesseract 的TESSERACT_…编译开关对齐别一边 DLL 运行时、一边静态运行时。最后说一句习惯以后每次接到一个依赖 ICU 的工程我都先把版本号、编译器、CRT 这三样写成一行注释贴在CMakeLists.txt顶上后面任何人接手都不用再翻压缩包的命名。希望这份拆解能帮你少踩几个坑一次把 Tesseract 的构建打通。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站