零、背景
之前因为工作需要,需要实现对公司某个产品的自动化登入,但因为登入需要输入验证码,当时——包括一直沿用到现在的操作是直接到后台数据库中写入一条token,绕过登入这一步。但私以为这是一种侵入性的操作,不仅需要后台的密码,以后转向容器化部署的话也不容易针对某一租户单独进行后台操作。所以我还是想走识别认证码、从前台登入获得token的路径,想着干脆试着用Java写成个服务提供API给部门使用:发送验证码图片的base64,识别后返回验证码内容。
先剧透一下:安装Tesseract和Leptonica依赖可直接避免此问题,但我希望我的服务能够开箱即用,能像Tess4j在Windows上运行时一样,在jar包中自带所依赖的动态链接库,所以执着于仅通过Java代码的修改解决依赖问题。
再剧透一下:没成功,目前似乎没有找到办法仅通过Java解决Tess4j在Linux上的依赖问题,先老老实实装/编译依赖吧……
一、最初的问题
且不论OpenCV进行图像预处理的过程,也不论未作预处理、不自行训练模型的识别准确性,单独用Tess4j简单写了一下识别的逻辑,在Windows下也能正常跑起来。但当我将打包好的应用放到Linux环境上运行时,则出现了如下报错:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Exception in thread "main" java.lang.UnsatisfiedLinkError: Unable to load library 'tesseract':
liblept.so.5: cannot open shared object file: No such file or directory
liblept.so.5: cannot open shared object file: No such file or directory
Native library (linux-x86-64/libtesseract.so) not found in resource path ([file:/root/ocr/quickOCR.jar])
at com.sun.jna.NativeLibrary.loadLibrary(NativeLibrary.java:301)
at com.sun.jna.NativeLibrary.getInstance(NativeLibrary.java:461)
at com.sun.jna.Library$Handler.<init>(Library.java:192)
at com.sun.jna.Native.loadLibrary(Native.java:672)
at com.sun.jna.Native.loadLibrary(Native.java:656)
at net.sourceforge.tess4j.util.LoadLibs.getTessAPIInstance(LoadLibs.java:85)
at net.sourceforge.tess4j.TessAPI.<clinit>(TessAPI.java:42)
at net.sourceforge.tess4j.Tesseract.init(Tesseract.java:424)
at net.sourceforge.tess4j.Tesseract.doOCR(Tesseract.java:220)
at net.sourceforge.tess4j.Tesseract.doOCR(Tesseract.java:192)
at main.main(main.java:21)
Suppressed: java.lang.UnsatisfiedLinkError: liblept.so.5: cannot open shared object file: No such file or directory
at com.sun.jna.Native.open(Native Method)
at com.sun.jna.NativeLibrary.loadLibrary(NativeLibrary.java:191)
... 10 more
Suppressed: java.lang.UnsatisfiedLinkError: liblept.so.5: cannot open shared object file: No such file or directory
at com.sun.jna.Native.open(Native Method)
at com.sun.jna.NativeLibrary.loadLibrary(NativeLibrary.java:204)
... 10 more
Suppressed: java.io.IOException: Native library (linux-x86-64/libtesseract.so) not found in resource path ([file:/root/ocr/quickOCR.jar])
at com.sun.jna.Native.extractFromResourcePath(Native.java:1145)
at com.sun.jna.NativeLibrary.loadLibrary(NativeLibrary.java:275)
... 10 more
/home/ubuntu/tess/tesseract-4.1.3/src/api/.libs/libtesseract.so.4.0.1
显而易见,是在调用doOCR的时候找不到相应的库。下载Tess4j的源码,在net.sourceforge.tess4j.util.LoadLibs.java中能找到这样一段内容(有删减和调整格式):
// File: net.sourceforge.tess4j.util.LoadLibs.java
private static final String VFS_PROTOCOL = "vfs";
private static final String JNA_LIBRARY_PATH = "jna.library.path";
public static final String TESS4J_TEMP_DIR = new File(System.getProperty("java.io.tmpdir"), "tess4j").getPath();
/**
* Native library name.
*/
public static final String LIB_NAME = "libtesseract413";
public static final String LIB_NAME_NON_WIN = "tesseract";
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(new LoggHelper().toString());
static {
System.setProperty("jna.encoding", "UTF8");
String model = System.getProperty("sun.arch.data.model",
System.getProperty("com.ibm.vm.bitmode"));
String resourcePrefix = "32".equals(model) ? "win32-x86" : "win32-x86-64";
File targetTempFolder = extractTessResources(resourcePrefix);
if (targetTempFolder != null && targetTempFolder.exists()) {
String userCustomizedPath = System.getProperty(JNA_LIBRARY_PATH);
if (null == userCustomizedPath || userCustomizedPath.isEmpty()) {
System.setProperty(JNA_LIBRARY_PATH, targetTempFolder.getPath());
} else {
System.setProperty(JNA_LIBRARY_PATH, userCustomizedPath + File.pathSeparator + targetTempFolder.getPath());
}
}
}
简单来说,判断系统是否为Windows,以及处理器架构(说实话这一段个人觉得写得不算很好,通过上面代码段里的方法只能获取JVM为32/64位,但32位的JVM也可以在x86-64的机器上运行),解压Tesseract的动态链接库到临时目录,然后将该目录加入JNA库路径。在Windows上执行了一遍,发现确实在%TEMP%\tess4j\win32-x86-64目录下解压了libtesseract413.dll和liblept1820.dll两个动态链接库。
看起来似乎加入Linux下的相关逻辑,然后放置Linux下的相应.so文件就行了?
并不!
二、嵌入Tesseract的动态链接
下载了tesseract4.1.3的源代码,编译得到libtesseract.so并放入资源目录下的linux-x86-64后,修改了一下net.sourceforge.tess4j.util.LoadLibs.java然后重新打jar包:
// File: net.sourceforge.tess4j.util.LoadLibs.java
System.setProperty("jna.encoding", "UTF8");
String model = System.getProperty("sun.arch.data.model",
System.getProperty("com.ibm.vm.bitmode"));
// =============FIX HERE=================
String osName = System.getProperty("os.name");
String resourcePrefixOs = "Linux".equals(osName) ? "linux" : "win32";
String resourcePrefixModel = "32".equals(model) ? "-x86" : "-x86-64";
String resourcePrefix = resourcePrefixOs + resourcePrefixModel;
// String resourcePrefix = "32".equals(model) ? "win32-x86" : "win32-x86-64";
// =============FIX HERE=================
放到Linux环境下再次运行,发现能解压出libtesseract.so到/tmp/tess4j/linux-x86-64下,但报错依旧存在——万幸是缩减了一点:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Exception in thread "main" java.lang.UnsatisfiedLinkError: liblept.so.5: cannot open shared object file: No such file or directory
at com.sun.jna.Native.open(Native Method)
at com.sun.jna.NativeLibrary.loadLibrary(NativeLibrary.java:277)
at com.sun.jna.NativeLibrary.getInstance(NativeLibrary.java:461)
at com.sun.jna.Library$Handler.<init>(Library.java:192)
at com.sun.jna.Native.loadLibrary(Native.java:672)
at com.sun.jna.Native.loadLibrary(Native.java:656)
at net.sourceforge.tess4j.util.LoadLibs.getTessAPIInstance(LoadLibs.java:85)
at net.sourceforge.tess4j.TessAPI.<clinit>(TessAPI.java:42)
at net.sourceforge.tess4j.Tesseract.init(Tesseract.java:424)
at net.sourceforge.tess4j.Tesseract.doOCR(Tesseract.java:220)
at net.sourceforge.tess4j.Tesseract.doOCR(Tesseract.java:192)
at main.main(main.java:21)
和最初的报错相比,少了libtesseract.so相关的报错,但liblept.so的报错依旧存在。
那么继续。
三、(起码目前)解决不了的Leptonica依赖和Library Path
将liblept.so打入jar包,并尝试了将其直接加入library path的方式,均无法成功:
// File: net.sourceforge.tess4j.util.LoadLibs.java
String javaLibraryPath = System.getProperty("java.library.path");
System.out.println(targetTempFolder.getPath());
if (null == javaLibraryPath || javaLibraryPath.isEmpty()) {
System.setProperty("java.library.path", targetTempFolder.getPath());
} else {
System.setProperty("java.library.path", javaLibraryPath + File.pathSeparator + targetTempFolder.getPath());
}
依然出现相同的报错,于是我去查看了一下net.sourceforge.tess4j.Tesseract中的doOCR和init函数发现它们仅仅是初始化了Tesseract实例;反复看了看net.sourceforge.tess4j.util.LoadLibs,也仅仅是加入对libtesseract.so的依赖。然后我意识到一个问题:虽然我知道Tess4j依赖Lept4j,但不至于在Tess4j初始化Tesseract实例的时候就需要liblept.so了吧?也没见net.sourceforge.tess4j.util.LoadLibs对Lept4j有依赖啊?
这时候我再去看了看Tess4j和Tesseract的文档,才发现了问题所在:Tesseract是使用了Leptonica的,且ld了一下libtesseract.so,发现它对liblept.so存在依赖。这下恍然大悟,但又生疑惑:我将liblept.so的路径也一同加入了library path,为了还是找不到liblept.so呢?
然后我找到了这篇回答(https://stackoverflow.com/questions/28131096/cannot-open-shared-object-file-c-library-in-java)。不那么严谨地说:在Linux和Windows下寻找链接库时逻辑不一样,是操作系统层面的逻辑差异而非单纯的Java特性。假如有一个native library在Linux下——就叫它libA.so吧,将其加入Java程序的library path后,Java程序是能寻找并调用libA.so的;但如果libA.so还依赖于libB.so,即便将libB.so的路径加入library path,由于这一层依赖并不受JVM控制,Java程序会在系统的library path中寻找libB.so。而在Windows下,如果调用A.dll,而A.dll又依赖于B.dll,只需将两者的路径都加入library path,Java程序便能寻找和调用这两个库了。在这里,libtesseract.so对liblept.so存在依赖,但在Linux下Java调用libtesseract.so时会在系统library path中寻找并不存在的liblept.so(在背景中介绍了,我希望我的服务开箱即用,无需在系统上安装额外依赖)。
当然,上面只是我目前的理解。由于还没有彻底研读Tess4j和Lept4j的源码,且调用doOCR后处理动态链接库依赖的逻辑也还没完全弄懂,我也不敢说一定没法通过“纯Java”的方式解决。回头打算再仔细看看Lept4j和Tess4j的依赖在Windows上具体是怎么处理的,说不定会有一定启发——就算不能解决问题,起码也学到东西了嘛。
P.S. 说起来这也是第一次去读别人的开源代码,我好像能体会到一点为开源软件做贡献的乐趣了hhhhhhh。我个人还是希望能完成开箱即用的移植,哪怕只对Ubuntu一个distro也好的。