PDF 是我们日常办公中常常接触的一种文件格式,本篇文章主要讲解如何使用 Apache 的开源插件 pdfbox 对 pdf 文档进行操作。同时也复习之前所学的内容,进行一个完整的电子发票处理流程:pdf转图片、图片识别、文件上传

首先在 pom.xml 中添加依赖:

<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.14</version>
</dependency>

当然本次功能主要实现的是 pdf 文件的读取功能,如果要写或生成 pdf 文件还需要引入其余的依赖包:

<!--字体-->
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>fontbox</artifactId>
    <version>2.0.14</version>
</dependency>
<!--pdf-->
<dependency>
    <groupId>com.lowagie</groupId>
    <artifactId>itext</artifactId>
    <version>2.1.7</version>
</dependency>

本次先写出控制层代码,然后再分步骤简介实现代码:

@PostMapping(value = "/vatInvoice")
@ApiOperation(value = "普通增值税发票识别(图片/pdf)")
public String vatInvoice(@ApiParam(value = "发票 图片/pdf", required = true) MultipartFile file) {
    if (file.getSize() > 2 * 1024 * 1024) return "大小限制:2M";

    // 获取文件名
    String fileName = Objects.requireNonNull(file.getOriginalFilename()).toLowerCase();
    // 获取文件后缀
    String suffix = fileName.substring(fileName.lastIndexOf('.'));
    // 文件上传到 OSS
    String name = "Temp/" + UUID.randomUUID().toString().replaceAll("-", "") + suffix;
    // 转换后的图片 base64, 文件上传链接
    String base64Str, fileUrl;

    if (suffix.equalsIgnoreCase(".pdf")) {
        base64Str = pdfUtil.pdf2Image(file);
    } else if (suffix.equalsIgnoreCase(".png")
            || suffix.equalsIgnoreCase(".jpg")
            || suffix.equalsIgnoreCase(".jpeg")
            || suffix.equalsIgnoreCase(".bmp")) {
        base64Str = imageUtils.image2Base64(file);
    } else {
        return "支持的格式:PDF、PNG、JPG、JPEG、BMP。";
    }
    if (Strings.isNullOrEmpty(base64Str)) return "格式转换有误";

    String data = baiduAIUtil.vatInvoice(base64Str);
    if (Strings.isNullOrEmpty(data)) return "识别失败";

    try {
        String ret = ossClientUtil.uploadFile2OSS(file.getInputStream(), name);
        if (Strings.isNullOrEmpty(ret)) return "上传失败";
    } catch (IOException e) {
        e.printStackTrace();
        return "上传出错";
    }

    fileUrl = ossClientUtil.getUrl(name);
    if (!fileUrl.startsWith("http")) return "获取失败";

    return "源文件上传:\n" + fileUrl.substring(0, fileUrl.indexOf('?')) + "\n识别结果:\n" + data;
}

运行截图如下:


本次测试用例:发票.pdf 和 发票.png。



控制器前几行代码自不必多说,限制上传大小获取文件名称初始化变量

接下来是根据文件后缀判断文件格式,如果是 pdf 格式就进入 pdfUtil.pdf2Image(file) 处理,将 MultipartFile 文件格式转化成图片,再编码成 Base64 格式;如果传入文件是图片类型,就调用 imageUtils.image2Base64(file) 将文件直接转成 Base64 格式。

pdf 转Base64 代码如下:

/**
 * PDF文件转PNG图片,第N页
 *
 * @param file      pdf文件 MultipartFile
 * @param pageIndex 第几页
 * @param dpi       dpi越大转换后越清晰,相对转换速度越慢 默认300
 * @return          Base64 String
 */
public String pdf2Image(MultipartFile file, int pageIndex, int dpi) {
    if (dpi <= 0 || dpi > 300) dpi = 300;
    PDDocument pdDocument;
    String base64Str = null;
    try {
        pdDocument = PDDocument.load(file.getInputStream());
        PDFRenderer renderer = new PDFRenderer(pdDocument);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BufferedImage image = renderer.renderImageWithDPI(pageIndex, dpi);
        ImageIO.write(image, "png", baos);
        base64Str = Base64Util.encode(baos.toByteArray());

        log.info("PDF文档转PNG图片成功!大小: " + base64Str.length() / 1024 + "KB");
    } catch (IOException e) {
        e.printStackTrace();
        return "";
    }
    return base64Str;
}

/**
 * @param file pdf文件 MultipartFile
 *             默认第一页; dpi: 300
 * @return base64 String
 */
public String pdf2Image(MultipartFile file) {
    return this.pdf2Image(file, 0, 300);
}

图片转 Base64 代码如下:

/**
 * MultipartFile图片转base64
 *
 * @param file MultipartFile类型的图片
 * @return base64字符串
 */
public String image2Base64(MultipartFile file) {
    InputStream is;
    ByteArrayOutputStream outStream;
    try {
        is = file.getInputStream();
        outStream = new ByteArrayOutputStream();
        // 创建一个Buffer字符串
        byte[] buffer = new byte[1024];
        // 每次读取的字符串长度,如果为-1,代表全部读取完毕
        int len = 0;
        // 使用一个输入流从buffer里把数据读取出来
        while ((len = is.read(buffer)) != -1) {
            // 用输出流往buffer里写入数据,中间参数代表从哪个位置开始读,len代表读取的长度
            outStream.write(buffer, 0, len);
        }
        // 对字节数组Base64编码
        return Base64Util.encode(outStream.toByteArray());
    } catch (IOException e) {
        e.printStackTrace();
    }
    return "";
}


接下来是调用 API 接口,发票图片识别获取发票信息:baiduAIUtil.vatInvoice(base64Str)

/**
 * 增值税发票识别
 *
 * @param img base64图片 (无uri)
 * @return 识别结果
 */
public String vatInvoice(String img) {
    try {
        Map<String, String> param = new TreeMap<>();
        param.put("image", URLEncoder.encode(img.replace("\r\n", ""), "UTF-8"));
        param.put("accuracy", "normal");
        String result = httpsPost(param, vat_invoice);

        return JSONObject.parseObject(result).get("words_result").toString();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "";
}

图片识别大致就是调用接口,然后 POST 发送信息,然后接收数据并返回。这部分前几篇文章详细的讲解过,所以此处不再详细讲解。可参考我以前的文章:SpringBoot API 系列(八) 发送请求


再往下就是上传源文件获取源文件的链接

上传源文件:ossClientUtil.uploadFile2OSS(file.getInputStream(), name)

/**
 * 上传到OSS服务器  如果同名文件会覆盖服务器上的
 *
 * @param instream 文件流
 * @param fileName 文件名称 包括后缀名
 * @return 出错返回"" ,唯一MD5数字签名
 */
public String uploadFile2OSS(InputStream instream, String fileName) {
    String ret = "";
    try {
        //创建上传Object的Metadata
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentLength(instream.available());
        objectMetadata.setCacheControl("no-cache");
        objectMetadata.setHeader("Pragma", "no-cache");
        objectMetadata.setContentType(getcontentType(fileName.substring(fileName.lastIndexOf("."))));
        objectMetadata.setContentDisposition("inline;filename=" + fileName);
        //上传文件
        PutObjectResult putResult = ossClient.putObject(ossBucketName, fileName, instream, objectMetadata);
        ret = putResult.getETag();
    } catch (IOException e) {
        log.error(e.getMessage(), e);
    } finally {
        try {
            if (instream != null) {
                instream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return ret;
}

获取源文件链接:ossClientUtil.getUrl(name)

/**
 * 获得url链接
 *
 * @param key
 * @return url
 */
public String getUrl(String key) {
    // 设置URL过期时间为10年  3600l* 1000*24*365*10
    Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 10);
    // 生成URL
    URL url = ossClient.generatePresignedUrl(ossBucketName, key, expiration);
    if (url != null) {
        return url.toString();
    }
    return null;
}


测试结果:

上传文件为 PDF 类型:


上传文件为图片类型: