Series Articles of Java Learning -- 01

Java项目实录01 -- 基于阿里云的表格识别OCR工具开发

Posted by OUC_LiuX on March 31, 2021

受崇教授所托,开发一款图片转表格OCR工具。决定调用阿里云的api,使用其java接口进行二次开发。项目代码开源在github
这篇博客主要记录项目开发的流程,一些共性的问题和基本语法问题会在其他博客中详细记录。

项目结构:

AliTableOCR   
|  
|--data   
|--lib     
|--out    
|--src    
|  |--com    
|  |  |--alibaba.ocr.damo    
|  |  |  |--AliTableOCR  
|  |  |  |--Base64excel  
|  |  |   
|  |  |--aliyun.api.gateway.demo.util  
|  |  |  |--HttpUtils  
|  |  |  |--pom.xml  
|  |  |  
|  |  |--gui   
|  |  |  |--FileChooser   
|  
|--jars...

代码解析

主函数类: AliTableOCR

方法: main()

public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
        @Override
        public void run() {
            FileChooser.createWindow();
        }
    });
}

由于class AliTableOCR的所有方法映射都到了GUI界面,从而项目main函数只有一个功能:启动GUI界面。并,通过EventQueue.invokeLater(Runnable)实现接口(Inference)Runnable中的run()方法保证main的线程安全。

静态方法: submit2AliAPI( String imgPath, String targetPath)

将图片编码为base64提交到阿里云API,并将返回的json数据解析为excel表格。

@ Params:   
@ imgPath:      接收图片路径    
@ targetPath:   目标表格路径

需要避免IO错误,但为查看实际解析出来的excel表格内容,此处不采取异常捕获机制,而是给targetExcel一个初始值。方便起见,目标表格路径需要和输入图片路径一一对应。由于输入图片限制为.jpg和.png格式,可以通过简单的字符串replace()方法实现,如66-69行代码:

else if (imgName.charAt(length-2)=='N') {
    targetExcel = targetPath + '\\' + imgName.replace(".PNG", ".xlsx");
}

API的输入接口为base64编码字符串,则需要将目标图片编码为base64:

String imgBase64 = "";
try {
    File file = new File(imgFile);
    byte[] content = new byte[(int) file.length()];
    FileInputStream finputstream = new FileInputStream(file);
    finputstream.read(content);
    finputstream.close();
    imgBase64 = new String(encodeBase64(content));
} catch (IOException e) {
    e.printStackTrace();
    return;
}

通过try-catch语句捕获输入输出异常。
通过new FileInputStream(file)创建文件file = new File(path)的输入流,并通过read(byte[])方法将文件流中的字节返回到字节数组中,字节数组的长度依照输入图片文件file而定。这是由于encondBase64(byte[])方法给出的输入接口是字节数组。
不要忘记关闭文件输入流。

下一步(row 114–134)将图片编码联合http头文件、请求、配置等等内容拼接成为json文件变量并通过JSONObject().toString()转化为字符串变量,目的是提交给云API。JSONObject变量可以通过json.put(key, body)加入JSONArray变量,其中JSONArray通过JSONArray.add(JSONObject)添加json变量为json数组。

接下来提交数据到云API并接收网络响应HttpResponse:

HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);    

response的状态码通过response.getStatusLine().getStatusCode()获取;主体内容通过response.getEntity()获取,通过http工具中的EntityUtils.toString(entity)得到相应的字符串数据。通过JSON类的静态方法JSON.parseObject(res)将上一步获得的字符串数据转化为JSONObject数据,这一步的目的是为了获取到主体内容中特定的value值,也即tables值。 该字符串数据是一串base64编码,通过Base64excel类中的 convert2excel(String base64code, String targetPath) 方法可以将之转化为目标excel文件:

String base64code = res_obj.getString(key="tables");    
Base64excel.convert2excel(base64code, targetExcel);

至此,单张图片转化为excel表格的核心功能完成。

类: Base64Excel

无需第三方jar包,该类只需要导入java.iojava.util.Base64即可。

静态方法: convert2excel(String base64, String output)

params
@base64:    接收到云API返回的base64编码    
@output:    输出的excel路径    

通过Base64包中的静态方法Base64.getDecoder()创建Base64.Decoder类对象。
定义并初始化文件输出流对象OutputStream out = null用以写入数据。
将文件输出流对象指向到output文件,此处有可能发生FileNotFoundException异常,使用try-catch捕获。
写入数据:

out.write(decoder.decode(base64));    

类: FielChooser: GUI界面

东西南北中五部分的Borderlayout界面,界面表现如图所示:

当然,这个GUI的 NorthPanel 由于列宽均等显得十分不和谐,项目实录2使用了一种更好的解决方案。

静态方法: creatWindow()

用于窗口布局:

JFrame frame = new JFrame("图片转表格OCR");
frame.setLayout(new BorderLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

createNorthPanel();
frame.add(northPanel, BorderLayout.NORTH);

createSouthPanel();
frame.add(southPanel, BorderLayout.SOUTH);

createCenterPanel();
frame.add(centerPanel, BorderLayout.CENTER);

frame.setSize(500, 400);
frame.setResizable(true);
frame.setLocationRelativeTo(null);
frame.setVisible(true);

静态方法: createNorthPanel()

窗口上方区域,用于选择/给定输入输出路径。
基本的提升GUI用户体验的选择是预先填充一个路径到路径文本输入框,而不是一个空的路径文本输入框。这一操作通过获取当前文件的绝对路径而实现:

File directory = new File("");     
path = directory.getAbsolutePath();      

其中path为静态全局变量,复制完成后,按键构造的监听器中可以直接使用。
显而易见的是,该区域( panel)应当分为两行三列: 输入一行,输出一行;Label(也即文本框前面的提示)一列,文本框一列,再加一个选择按钮,可以通过鼠标点击而不是键盘键入的方式填充路径选择文本框。于是:

northPanel = new Panel(new GridLayout(2, 3));   

为保证观感,我们希望标签在第一列应当靠右,文本框在中间一列应当靠左:

northPanel.add(new JLabel("输入路径:", SwingConstants.RIGHT));   
northPanel.add(picPath, BorderLayout.WEST);   

按钮通过静态方法createButton(String name, JTextField textField) 构造。

静态方法: createButton(String name, JTextField textField)

用以构造路径选择按键,按键可以打开一个路径选择框,并将选定的路径自动填充到路径文本框中。
在按键事件监听器中添加包含下拉对话框的路径选择组件:

button.accActionListener(new ActionListener(){
    @Override
    public void actionPerformed(ActionEvent e){
        JFileChooser fileChooser = new JFileChooser(path);
        fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);  
        // 文件选择器设定只选择路径   
        int option = fileChooser.showOpenDialog(null);
        if(option == JFileChooser.APPROVE_OPTION){
            File file = fileChooser.getSelectedFile();
            textField.setText(file.toString());
            // 给路径输入框赋值为选中的路径   
            if (name == "输入"){
                FileChooser.path = textField.getText();
                FileChooser.outputPath = textField.getText().replace(
                        "data", "output");
                excelPath.setText(outputPath);
            }
            else if ( name == "输出"){
                FileChooser.outputPath = textField.getText();
            }
        }
        else{
            textField.setText("请重新选择");
        }
    }
})     

静态方法: createCenterPanel()

创建信息输出窗口,这里有两个需要注意的地方。一个需要给是文本域JTextArea()设置自动换行:

JTextAreaObject.setLineWrap(boolean);  // 默认为false,不自动换行            

当布尔参数为false的时候,将不自动换行;true激活该功能。另外需要了解的是,对于英文字符自动换行有两种风格,可以通过

JTextAreaObject.setWrapStyleWord(boolean);  //默认为true

设置。布尔参数为true意思是在单词边界处换行(每行最右侧可能留白),当设置为false时候在字符边界处换行,右侧不留白,但最右侧单词可能会被分成两部分显示。

另一个是需要注意的是把设置好的文本域添加到滚动条panel里面:

JScrollPane scorllPane = new JScrollPane(JTextAreaObject)   

滚动条默认是当文字超出范围文本域后才显示,但可以通过以下语句主动激活其常显示功能:

scrollPane.setVerticalScrollBarPolicy( 
    JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);   

命令意思是设置垂直滚动条策略,参数有:

@ params:  
@ JScrollPane.VERTICAL_SCROLLBAR_ALWAYS     // 常显示    
@ JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED  // 当需要时显示(默认)    
@ JScrollPane.VERTICAL_SCROLLBAR_NEVER      // 从不显示   

额外的,我们还可以类似地设置水平滚动条策略:

scrollPane.setHorizontalScrollBarPolicy();    

参数相应地为:

params:   
@ JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS   
@ JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED   
@ JScrollPane.HORIZONTAL_SCROLLBAR_NEVER       

最后将scrollPane组件加入到centerPanel
方法中还有一条语句是centerPanel.serBorder(new EtchedBorder),是设置边框显示风格用的,去掉无碍。

静态方法: createSouthPanel

最下面一层panel,内容很简单,实现“开始”和“退出”两个按键。其中:
“开始”按键需要添加用来遍历文件、执行主类方法、并打印相关信息到文本信息域的事件监听器,“退出”按键添加退出功能的事件监听器。

for(variable: collection)方式遍历路径下所有非文件夹文件:

File files = new File(path);        // 获取路径文件
File[] fs = files.listFiles();      // 获取文件列表
for (File f: fs){                   // 遍历文件
    if( !f.isDirectory() {          // 判断是否是次级路径
        AliTableOCR.submit2AliAPI() // 调用主类方法
        infoPrint.append(String.format("%s 完成转换\n", f.toString()));
        // 添加信息到文本域   
        infoPrint.paintImmediately(infoPrint.getBounds());
        // 实时刷新文本域,如果不,程序执行期间不会更新文本域,
        // 而是积累到结束后一次性打印
    })
}