1 引言
日常开发中接到这样的需求,上游系统请求获取一张A4单据用于仓库打印及展示,要求PNG图片格式,但是我们内部得到的单据格式为PDF,需要提取PDF文档的元素并生成一张PNG图片。目前已经有不少开源工具实现了这一功能,我们找了网上使用比较多的Apache PDFBox库来实现功能,如下
// Step 1
PDDocument document = PDDocument.load(content);
PDFRenderer pdfRenderer = new PDFRenderer(document);
// 获取第1页PDF文档
OutputStream os = new ByteArrayOutputStream()
// Step 2
// 为了保证图片的清晰,这里采用600DPI
BufferedImage image = pdfRenderer.renderImageWithDPI(0, 600);
// Step 3
ImageIO.write(image, "PNG", os);
实际测试时,明显感觉到卡顿,当一次请求的单据数目较多时尤其严重。
经统计,各步骤本机单次运行耗时如下:
pdf 初始化(Step 1):2ms
文档提取及图片绘制(Step 2):520ms
图片编码 (Step 3):3823ms
我们发现,最后一句代码耗时接近4秒,拖累了整体性能。我们要如何优化这样一个问题呢?
2 BufferedImage介绍
在讨论优化问题之前,首先要搞清楚待优化的代码是做什么的。如上代码中,使用renderImageWithDPI方法,将文档元素绘制为BufferedImage对象。
根据描述,BufferedImage用来描述一张图片,其内部保存了图片的颜色模型(ColorModel)及像素数据(Raster)。这里简单解释就是,内部的Raster实现类中,以某种数据结构(如Byte数组)表示图片的所有像素数据,而ColorModel实现类,则提供了将每个像素的数据,转换为对应RGB颜色的方式。
BufferedImage的构造函数中,可以传入图片类型来决定使用哪一种ColorModel和Raster。引言的示例中,PDFRender源码中默认生成的图片类型为 TYPE_INT_RGB,这种类型表示,每一个像素使用R、G、B三条数据表示,每条数据使用单字节(0~255)表示。
public BufferedImage(int width, int height, int imageType)
需要注意的是,BufferedImage并不表示某一张具体的位图,而是通过描述每个像素的数据,抽象地表达一张图片,因此,它可以在内存中通过操作像素数据,直接改变对应图片。而通过ImageIO.write方法,可以将BufferedImage编码为具体格式的图片数据流。此方法会根据formatName选择该文件格式的编码器,来对BufferedImage内部的像素数据进行编码。
public static boolean write(RenderedImage im, String formatName, OutputStream output) throws IOException
以下代码为BufferedImage的简单应用
将一个GIF图片读取到BufferedImage中,在坐标(10,10)位置打出ABC三个字符,并重新编码成PNG图片
BufferedImage image = ImageIO.read(new File("exmaple.gif"));
image.getGraphics().drawString("ABC", 10, 10);
ImageIO.write(image, "PNG", new FileOutputStream("result.png"));
下面这段代码展示了另一类型的例子,它将图片中所有的红色像素点重置成黑色像素点
BufferedImage image = ImageIO.read(new File("example.gif"));
for(int i = 0 ; i < imag