Smart Crop,一种切除 PDF 扫描文档白边的新选择(工程篇)

周日深夜,我把代码分享到了 Github,用的 MIT 协议,详见 JamesPan/pdf-smart-crop。原本还想着把注释文档和单元测试写了再分享代码的,后来实在是懒了。所以说啊,这些东西如果开发的时候不好好写,以后就更没有动力去写了。

前作「Smart Crop,一种切除 PDF 扫描文档白边的新选择(算法篇)」分享了 Smart Crop 的算法设计、基本用法和脑洞,这里分享一下实现过程中遇到的问题和妥协。

Why not Python

之前说过我在算法设计和调试阶段用的是 Python,到了工程实现却选择了 Java。为什么没用 Python?

先看一下程序的输入和输出。其实不用看,输入和输出都是 PDF 文档,但是中间步骤是什么呢?

用 Python 处理 PDF 文档,我们有几个类库可供选择,比如 pyPDF、pdfrw 之类,但是我当时就没找到一个纯 Python 实现的能够把 PDF 文档给一页页渲染成图像的类库。要么就是依赖 ImageMagick,要么就是依赖 poppler,要么就是臣妾做不到啊。

ImageMagick 是一个包罗万有的图像处理工具包,好像是 C 语言写的,主流平台都能安装。但是从软件分发的角度想想,当时我还是想写一个面向大众的软件的,总不能让用户先把 ImageMagick 装上再运行我写的软件吧?

poppler 是一个 C 写的 PDF 类库,很有名,可惜是 C 写的。C/C++ 写的代码就意味着可执行文件不跨平台,我 release 一次就得整出三个平台的可执行程序,对于我这么懒的人来说简直要命。更要命的是我尝试安装 python-poppler-qt4 这个类库居然还失败了!

虽然我深爱着 Python,但最终也不得不放弃,考虑其他语言。

Why Java

Node.js 如何?天生跨平台,写 GUI 有大名鼎鼎的 NW.js,曾经的 Node-Webkit,写 CLI 更是简单到爆。只可惜,NPM 上面找到的能处理 PDF 的类库,无一例外是对 ImageMagick 的包装。

把我引向 Java 的,是一篇来自 reddit 的讨论,Best current tools for working with PDF files in python?

最佳答案说,当前用 Python 去完成把 PDF 转成图像这个工作不是一个好想法,Apache PDFBox 大法好。

Apache keeps their shit updated and documented as well.

既然寻寻觅觅了这许久,最后发现大家都公认,把 PDF 转成图像非 PDFBox 莫属,我也就不用纠结了,直接用 Java 吧,反正 Java 是最好的语言,上能 GUI 下能 CLI 还能写 REST API(逃。

而且 Java 在软件分发的时候也是相对简单的,直接发布一个可执行的 jar 包即可,大不了用 packr 把 JRE 给一起打包了再分发。

DI with Guice

把一段已经写的差不多的 Python 翻译成 Java,就会深刻体会到两种语言之间的差异是多么的巨大。总之一段百来两百行的 Python 代码,到了 Java 里面就成了错综复杂的接口和实现。

有了接口有了实现,就得面向接口编程,依赖注入顺势登场。有了依赖注入的需求,就需要一个 IoC 容器。之前在工作中写 Java Web 的时候,用的容器自然是 Spring。但是这次我想来点不一样的,之前听说 Google 出品的 Guice 是一个轻量级的 DI 框架,如果这次不试一下,可能就错过机会了。

Guice 的用法还蛮有趣的,从之前研究 Guava 开始我就发现 Google 出品的 Java 类库似乎喜欢用各种奇技淫巧搞 DSL,Guice 自然也不能免俗。

public class CropModule extends AbstractModule {
  @Override
  protected void configure() {
    bind(ImageProcessor.class).to(DefaultImageProcessor.class);
  }
}

除去类定义之类的废话代码不看,上面这段代码中唯一有用的那行,不就是一个有趣的 DSL 么,把接口和实现给 bind 到了一起。

在 Spring 中,托管的 Bean 如果不做额外声明,默认都是单例。但是 Guice 不一样,如果需要单例,需要额外注解一下。

@Singleton
public class DefaultImageProcessor implements ImageProcessor {
  ...
}

其实让我感觉最大的不同,是最佳实践上的。Guice 推荐在构造函数中注入,而且保证不回注入空指针,而 Spring 推荐在 Setter 中注入,如果没有注入自然就是空指针了。Guice 推荐构造函数仅包内可见,包外需通过 IoC 容器获取实例,无法直接构造对象,而 Spring 似乎没有这种说法。

也许是这种最佳实践和真正的工程实践相互作用,使得使用 Guice 作为 DI 的项目大多是拥有良好模块化设计的基础设施,比如 Elasticsearch;而使用 Spring 最为 DI 的项目大多是金玉其外的企业级项目。

发现 IDEA 的一个关于 Guice 的彩蛋。

左侧的边栏上有三个插头,鼠标点一下就会直接跳转到这个实例被注入的位置!好形象好有趣的样子。

CLI Framework

最开始的时候我是打算写一个能够给大众使用的软件的,最后由于时间关系,等不及写 GUI 了,我就只好写了个 CLI 作为交互界面。

那么问题来了,Java 写 Shell 程序,有什么好用的框架把解析命令行参数、生成 help 内容之类的琐事给包办了么?

当然是有的,而且还不止一个的样子。很早我就知道 Spring 有一个 Spring Shell,这次一番考察之后我还发现了 Clamshell-Cli。最后选择了 Spring Shell,因为 Clamshell-Cli 那种插件化的架构虽然扩展起来简单,但是用起来似乎有点麻烦,不容易以 fat jar 的形式分发。

Spring Shell 其实蛮好用的,自定义启动 Banner、Prompt 还有历史记录文件之类的都很简单,继承默认实现,覆盖几个关键函数就好了,需要注意的是把这些自定义的 Banner 类注册成 Spring Bean,启动优先级设置为最高。

实现具体的命令,就需要继承 CommandMarker,然后用注解给出指定命令的元数据,比如命令明,帮助信息,各个参数以及参数解释之类的。Spring Shell 会自动采集这些元数据,帮我们处理参数解析、生成 help 之类的杂事。

我在实现 Smart Crop 的命令行工具的时候发现两个有趣的点,在这里分享一下。

第一个是输出处理进度。

大四的时候学过人机交互,教材用的是「人本界面」,依稀记得有一个说法,计算机响应指令要尽可能快,如果是在快不了,要给个进度条让用户知道计算机没死机。

在 CLI 下如何输出进度条呢?我之前并没有这方面的经验,只好 Google 之,最后还是从 StackOverflow 找到了答案,问题的关键就在于之前一直被我忽视的回车符 \r

还记得刚上大学那会,第一门课程是「计算机科学导论」,授课教师是王建荣教授。至今依然记得王老师当时解释不同平台上文本换行的区别时说,Windows 上的换行 \r\n 是回车换行,想象一下打字机,先把打印头推回行首,这叫回车,然后向下移动一行,这叫换行。

当时没有去想更没有去尝试,假如只输出换行不输出回车会是怎样一种结果,就只是记下了 Windows 和 Unix 之间在换行上的差异。当然更多的时候轮不到我们去关心这些细微的差异,底层类库早已处理妥当。

曾经让我自我感觉良好的,是我从一开始就学习最佳实践,然后实践最佳实践,最后得到一套合适的实践。如今想来,一开始就实践最佳实践,固然能够让我少走弯路走得比别人快一步两步,但是也错过了一些弯路上别致的风景。

第二个是版本号。

就是这个版本号,1.0.1。最开始的时候,我是覆盖了 Spring Shell 的方法,直接返回了一个字符串常量。但是后来我感觉这样不合适。

项目的版本号本来已经在 POM 文件里面定义了一次,为什么还需要在代码里面写一次?且不说一份数据维护在两个地方本身就是个大坑,光是每次升级版本都要记得去修改返回版本号的函数就让人受不了。

有没有方法直接让版本号和 POM 中的版本号一致,或者说在 Java 代码中获取 POM 中的版本号呢?我打算从 Spring Shell 的默认实现开始研究。

public class VersionUtils {
  public static String versionInfo() {
    Package pkg = VersionUtils.class.getPackage();
    String version = null;
    if (pkg != null) {
      version = pkg.getImplementationVersion();
    }
    return (version != null ? version : "Unknown Version");
  }
}

上面这段代码就是默认实现中真正工作的代码,居然是通过反射获取包,然后获取包的版本。还有这等黑科技!于是我连忙把覆盖函数删掉,重新运行。

这个 Unknown Version 是什么鬼啊 (╯°□°)╯︵ ┻━┻ 看起来是反射拿到的版本信息为空了,但是为什么是空呢为什么呢为什么呢?

假如突然没有了 Google,没有了 StackOverflow,我就变成了个不会写代码的渣渣。不过这一次不是在栈溢出找到的答案

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <configuration>
    <archive>
      <manifest>
        <addDefaultImplementationEntries>
          true
        </addDefaultImplementationEntries>
      </manifest>
    </archive>
  </configuration>
</plugin>

在 build 阶段增加一个 maven-jar-plugin 插件,然后版本信息就会写入 jar 包的元数据中,然后就能够被 Java 代码反射读取了!

果然不放过任何一个偷懒的机会才能学到更多的新姿势!

时间: 2025-01-27 02:53:35

Smart Crop,一种切除 PDF 扫描文档白边的新选择(工程篇)的相关文章

Smart Crop,一种切除 PDF 扫描文档白边的新选择(算法篇)

这是我元旦假期的折腾成果.这里先分享一下思路和实现过程中遇到的有意思的事情,代码稍后整理后分享到 Github. 前些日子,同事送了我一个 Kindle,于是我开心地往里面灌了好几本书,开始假装文化人. 背景 但是在尝试阅读的时候,我发现体验并不怎么好,因为我平日里看的电子书大多是扫描版的以技术为主的各类书籍,这些扫描书有一个共同点,就是有比较宽的白边(margin).于是我们在阅读这类电子书的时候通常会用各种手段把白边切掉,以便让内容在本来就不大的屏幕上占据更多像素. 相关工作 之前用 iPa

《Adobe Acrobat XI经典教程》—第2课制作可编辑和搜索的扫描文档

制作可编辑和搜索的扫描文档Adobe Acrobat XI经典教程从Microsoft Word或Adobe InDesign等应用程序中将文件转换成PDF时,文本是完全可以编辑和搜索的.但是,图像文件中的文本,无论是以图像格式保存的扫描文档还是文件,都无法编辑和搜索.利用OCR(光学字符识别)技术,Acrobat将可以分析图像,并用不连续的字符代替图像的某些部分.OCR同时还可以识别分析得可能不正确的字符. 下面,我们把OCR技术用在之前转换过的TIFF文件上. 1.选择File>Open,导

估值40亿美元的Dropbox又为用户带来新功能:AI识别扫描文档

  云文件同步和共享服务商Dropbox今天披露了更多支持光学字符识别(OCR)功能的技术细节,已经为Dropbox Business付费的企业员工可以在Android和iOS应用程序中使用该功能. 具体操作是这样的,使用移动设备上的相机扫描文档后,光学字符识别功能将会启动.然后,应用程序会根据需要裁剪或旋转文档,然后将其保存为Dropbox中的PDF.8月,该公司表示正在使用计算机视觉来检测应用程序扫描文档. 与人工智能深度学习结合的OCR技术已经不是新鲜事了.GitHub 上的开源软件可以用

在Word2016中快速编辑PDF格式文档

  在Word2016中快速编辑PDF格式文档           首先在资源管理器中找到想要编辑的PDF文件,右击它,从右键菜单中选择"打开方式→Word 2016". Word将自动启动,并弹出一个提示框,大意是转换后的文档与原PDF文档的格式会有些不同. 点击"确定",Word开始打开操作,如果PDF文档很大,这个过程会有些漫长.当PDF文档转换完成后,可能还会在上面出现安全提示,询问你是否确定要编辑. 如果需要编辑,可以点击"启用编辑"按

word2016怎么直接编辑PDF格式文档?

  微软最新款办公软件:office2016为用户带来了非常多实用的功能,其中,让小编印象最深刻的就是,不需要将PDF格式文档转换称word格式,使用office2016的word2016就可以直接编辑PDF文档.那么,word2016怎么才能直接编辑PDF文档呢?一起来看看今天的教程吧! 1.电脑必须安装office2016套件下的word,然后准备好你想要转换的pdf文件. office2016 2.用鼠标右键单击pdf文件,依次打开"用其他方式打开">选择word2016.

Dreamweaver文档代码模式中文字体选择不准确解决办法

将编辑器默认的编码设置为"简体中文" 位置:Dreamweaver菜单栏>首选项>默认编码文档代码模式中文字体选择不准确解决办法-dreamweaver帮助文档"> 使用Dreamweaver时,一开始以为是软件的bug,后来在网上寻求解决方法才知道,原来是由于DW新建文档默认unicode(UTF8)在代码编辑器不好选中的缘故.改为GB就行了. 不过据说使用GB打开文件可能会有乱码的情况,Dreamweaver的代码编辑辅助功能虽然强大,不过最好还是有其它

总结了几种常用的Word文档转PDF方法

在日常办公中,我们接触的大多都是要将PDF文件转换为Word文件,那么怎么将Word文档转换为PDF文件呢?电脑爱好者就针对已安装 Microsoft Office 环境的情况,介绍一些相对简便的Word转PDF思路,当然如果你正在使用 Adobe Acrobat 的话,以下的方法就可以无视了. 1.安装有Microsoft Office 2003环境 Office 2003需要 PDF 虚拟打印机的支持,安装虚拟打印机后,选择文件->打印,在打印机列表中选择 PDF 虚拟打印机,即可输出为 P

word2016怎么直接编辑PDF格式文档

  1.准备好你想要转换的pdf文件,用鼠标右键单击pdf文件,依次打开"用其他方式打开",然后选择word2016. 文档-word2016文档无法编辑"> 2.之后会在打开word主界面弹出pdf转换为word后的注意须知.原文件越大,转换时间越久,图片越多越久. 3.转换过程持久的,需要耐心等待,如果想取消,可以按ESC键.实际转化后的结果如图.

Kuzzle,一种内部部署的文档后端

Kuzzle是一种可以内部部署或是在云中运行的文档后端.在近期的CES 2017上,提供该平台的公司公布了其企业版解决方案. Kuzzle用NoSQL仓库对文档做持久保存,支持基于模式的或是无模式的文档.Kuzzle提供CRUD API,并使用了Elasticsearch提供高级搜索特性.数据可以跨设备和协议同步.Kuzzle的特性包括实时的文档更改通知,并可使用插件扩展特性.插件使开发人员可以在操作执行前或执行后添加功能.扩展API等.Kuzzle支持HTTP.WebSocket.Socket