成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

一次單據圖片處理的優化實踐

開發 前端
通過對問題的優化,對以PNG為例的位圖文件結構,和Java中對圖片的基本操作有了漸進式的理解;同時也意識到,日常工作中,通過對業務本身的理解,清楚知道業務的邊界在那里,加上對技術基礎知識的深入理解,才能更細致地針對性做出優化。

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 < image.getWidth() ; i++) {
   for(int j = 0 ; j < image.getHeight() ; j++) {
       if(image.getRGB(i, j) == Color.RED.getRGB()) {
          image.setRGB(i, j, Color.BLACK.getRGB());
       }
   }
}

如果我們想要取得圖片的數據,可以通過BufferedImage內部的Raster對象獲得。下面的示例,展示了采用了字節數組形式存儲時,取得內部存儲的字節數組的方式。注意,當需要查詢到某一個像素的數據時,需要綜合像素的x,y坐標及ColorModel模型中像素數據的存儲方式來決定數組下標。

BufferedImage im = ImageIO.read(new File("exmaple.gif"));
DataBuffer dataBuffer = im.getRaster().getDataBuffer();
if(dataBuffer instanceof DataBufferByte) {
     DataBufferByte bufferByte = (DataBufferByte) dataBuffer;
     byte[] data = bufferByte.getData();
}

那么,現在我們可以通過看源碼,了解引言的示例代碼的作用。

根據源碼可以了解到,PDFRender對象讀取并識別PDF文檔中的每條語句,利用BufferedImage中的Graphics2D重新畫了一張圖片,并編碼成PNG格式。這里不詳細說了。

3 PNG文件格式淺析

根據上一節的內容可知,把BufferedImage編碼成PNG文件的過程,耗時接近2秒。我們需要簡單了解下編碼PNG文件的過程中,究竟在干什么。

以下參考W3C上對PNG的描述 https://www.w3.org/TR/PNG/#11IHDR ,由于比較復雜,很多東西我也是一知半解,這里僅描述本次優化涉及到的主要內容。

PNG文件可以包含很多數據塊,最主要且必須包含的,是IHDR,IDAT及IEND三個數據塊

我們通過十六進制打開PNG文件,就可以看到具體的數據塊分布

  • IEND
  • IEND為結束標志
  • IHDR

IHDR為文件頭,其后緊跟的字節描述了PNG文件的一些基礎屬性,如寬、高各占4各字節,而Color type和Bit Depth分別表示顏色類型和位深。

1.Colour type顏色類型分為以下幾種:

Greyscale為灰度圖,每個像素用單一的灰度值來描述顏色,灰度值由0(白)到255(黑)逐步加深。

Truecolor即為一般的RGB三通道圖片,R、G、B每一個通道允許用8或16個比特來表示。

Indexed-color為索引色,需要配合調色板PLTE數據塊使用,這里不多做介紹。

后面兩種Greyscale with alpha, truecolor with alpha,顧名思義,即灰度和RGB圖像增加透明度通道

2.Bit Depth(位深度),即每個通道使用多少比特來表示。

比如在一張Colour type=Greyscale中,一個像素由1~255的灰度值來表示,那么這張圖片就是單通道8位深。

根據上表,我們知道位深度于顏色類型是有相關性的。比如Greyscale灰度圖只能支持1,2,4,8,16位深。

3.Compression Method壓縮算法

后面的Compression Method為數據壓縮算法,固定為zlib LZ77算法。該算法通過編碼一定范圍內的重復數據來壓縮整體數據,有興趣的同學可以了解一下,這里不多做介紹了。找了一張網上的解說圖,通過此圖可以大致了解此壓縮具體在做些什么。

LZ77算法可以設置一個壓縮級別參數,參數范圍為0 ~ 9,其中0為不壓縮;1為最快速度,但壓縮率較低;9為壓縮率最高,但速度會相對較慢。

4.Filter Method過濾方法

過濾方法即壓縮前的預處理,主要目的是對于一些顏色變化比較“陡”的圖片,通過一些數據的變換增加像素數據的重復度,從而增加壓縮率。

試想一個場景,一張圖片每一個像素點都是前一個像素顏色的遞增,那么這張圖片每一個像素點都是不同的數值,按照上面的壓縮方法,它將無法被壓縮。而如果我們對它進行預處理,以第一個像素為基準,后面每一個像素點均變換為當前像素與前一個像素的差值,那么這個變換是可逆的,并且會人為創造出大量的重復數據便于壓縮。

具體這些過濾方法為什么可以增加重復數據,由于不涉及此次優化,我也沒有做深入了解。后面可以看到,因為我們業務場景本身的原因,并不需要預處理。

IDAT

IDAT數據塊為真正的圖片像素數據,這部分數據是經過過濾(Filter)及壓縮(Compresson)的,這些方法都有比較成熟的實現,我們也不考慮在這里做任何優化了,因此不多做介紹。

4 優化方案

經過上述內容,針對引言中的問題,我們確定了2個優化方向

  • 業務上,無論怎樣的單據,都是要倉庫打印的,基本都是黑白圖片。PNG的顏色類型使用Truecolor是冗余的,根據上圖中IHDR文件頭表格內容可知,PNG圖片是支持灰度(Greyscale)同時位深為1的,即每個像素點由1比特來表示(0代表白點,1代表黑點)。這樣可以減少PNG文件的體積,以及壓縮生成IDAT塊的時間。
  1. 調整zlib壓縮算法的級別為1,犧牲壓縮率來提高速度

經過查看源碼,當BufferedImage的imageType=TYPE_BYTE_BINARY(二進制)時,JDK中的PNG編碼器會使用灰度的color type及1位深,而我們發現PDFRender類是有參數可控的,當傳入BINARY時,繪制的BufferedImage的類型即為TYPE_BYTE_BINARY。

BufferedImage image = pdfRenderer.renderImageWithDPI(0, 304, ImageType.BINARY);

使用此方法后,ImageIO.write編碼過程耗時減少到150ms左右。

但是這樣改后,我們發現生成的PNG圖像,與原PDF文檔在觀感上相比,有一些發“虛”,如下圖

PDF截圖

PNG截圖

由于TYPE_BYTE_BINARY類型的BufferedImage每個像素只由0,1來表示黑白,很容易想到,這個現象的原因是出在判斷“多灰才算黑”上。

我們來看一下源碼中,BINARY類型BufferedImage的ColorModel,是如何判斷黑白的。BINARY類型的BufferedImage使用的實現類為IndexColorModel, 確定顏色的代碼段如下,最終由pix變量決定顏色的索引號。

int minDist = 256;
int d;
// 計算像素的灰度值
int gray = (int) (red*77 + green*150 + blue*29 + 128)/256;
// 在BINARY類型下,map_size = 2
for (int i = 0; i < map_size; i++) {
 // rgb數組為調色板,每個數組元素表示一個在圖片中可能出現的顏色
 // 在BINARY類型下,rgb只有0x00,0xFE兩個元素
    if (this.rgb[i] == 0x0) {
        // For allgrayopaque colormaps, entries are 0
        // iff they are an invalid color and should be
        // ignored during color searches.
        continue;
    }
    // 分別計算黑&白與當前灰度值的差值
    d = (this.rgb[i] & 0xff) - gray;
    if (d < 0) d = -d;
    // 選擇差值較小的一邊
    if (d < minDist) {
        pix = i;
        if (d == 0) {
            break;
        }
        minDist = d;
    }
}

由以上代碼,在JDK的實現中,通過像素的灰度值更靠近0和255的哪一個,來確定當前像素是黑是白。

這種實現方式對于通用功能來說是合適的,卻不適合我們的業務場景,因為我們生成的圖片都是單據,大部分需要倉庫等場景現場打印,需要優先保證內容的準確性,即不能因為圖片上某一處灰得有點“淺”,就不顯示它。

對于當前業務場景,我們認為簡單地設置一個固定的閾值,來區分灰度值是一個適合的方式。

所以,為解決這個問題,我們設計了2種思路

  1. 繼承實現自己的ColorModel,通過閾值來指定調色板索引號,所有要編碼成PNG的BufferedImage都使用自己實現的ColorModel。
  2. 不使用JDK默認的PNG編碼器,使用其他開源實現,在編碼階段通過判斷BufferedImage像素灰度值是否超過閾值,來決定編入PNG文件的像素數據是黑是白。

從合理性上看,我認為1方案從程序結構角度是更合理的,但是實際應用中,卻選擇了方案2,理由如下

  1. BufferedImage通常不是自己生成的,我們往往控制不了其他開源工具操作生成的BufferedImage使用哪種ColorModel,比如我們的項目里PDF Box,IcePdf, Apache poi等開源包都會提供生成BufferedImage的方法,針對每個開源工具都要重新更改源代碼,生成使用自己實現的ColorModel的BufferedImage,太過于繁瑣了,不具有通用性。
  2. JDK提供的PNG編碼器不能設置壓縮級別

5 實際優化過程

我們通過網上搜到了開源Java實現的PNG編碼器pngencoder作為此業務場景下的編碼器。

<groupId>com.pngencoder</groupId>
<artifactId>pngencoder</artifactId>
<version>0.14.0-SNAPSHOT</version>

但是我們發現一個問題,開源實現的PNG編碼器在編碼BufferedImage時,為了方便整字節進行操作,基本都是只能支持8或16比特的位深的PNG,無法支持我們需要的1比特的位深. 經過分析,這一點可以通過自己開發簡單的代碼實現來補充,因為無論使用幾位深,最終PNG編碼都是針對像素數據整理過后,對整字節的數據進行后續的過濾及壓縮來生成IDAT數據,因此,我們只需要實現對原BufferedImage像素數據的提取并轉換為1比特位深度這一步驟。

因此,我們的需求就是,針對一個BufferedImage,每個像素的灰度值通過與閾值比較大小,映射為一個bit數組,并將bit數組轉換為byte數組。下面是我們借助這個開源工具內部實現的部分代碼:

/**
* 在開源工具原有代碼基礎上,判斷1bit位深時,使用另外的像素數據收集方法
*/
case TYPE_BYTE_GRAY:
    if(bitDepth == 1) {
// 針對灰度圖像,當位深為1的時候走自己實現的數據獲取方法
// RGB圖像也可用類似方式
        getByteOneBitGrey(bufferedImage, yStart, width, heightToStream, consumer);
    } else {
// 原代碼
        getByteGray(bufferedImage, yStart, width, heightToStream, consumer);
    }
    break;

自定義1bit位深取數據方法

/**
* 生成使用1bit位深,Greyscale的PNG的像素數據
* 當IHDR中bit Depth為1時,使用這個方法來生成IDAT的原始數據
* @param image 圖片BufferedImage
* @param yStart 從圖片哪一行開始掃描
* @param width 圖像寬度
* @param heightToStream 待處理的高度
* @param consumer 原始數據塊后續處理函數
*/
static void getByteOneBitGrey(BufferedImage image, int yStart, int width, int heightToStream, AbstractPNGLineConsumer consumer)
        throws IOException {
    // 字節數組的長度
    int rowByteSize = (int) Math.ceil(width / 8.0);
    byte[] currLine = new byte[rowByteSize + 1];
    // BufferedImage Raster像素數據
    byte[] rawBytes = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
    int currLineIndex, bitIndex;
    byte currValue = 0;
    for(int y = 0 ; y < heightToStream ; y++) {
        int start = (yStart + y) * width;
        currLineIndex = 0;
        bitIndex = 0;
        // 這里有一個坑,PNG數據每行要以一個額外的0x00開頭
        currLine[currLineIndex++] = 0;
        for (int i = 0; i < width; i++) {
 // 查到當前像素的灰度值,150為手動設置的閾值,小于150則認為是白色
            byte bitVal = (byte) ((rawBytes[start + i] & 0xFF) < 150 ? 0 : 1);
 // 把每個像素的bit合并到一個byte中
            currValue |= bitVal << (7 - bitIndex++);
            // 當取了8個bit時,將一個完整的byte放入待處理數據
            if (bitIndex == 8) {
                currLine[currLineIndex++] = currValue;
                currValue = 0;
                bitIndex = 0;
            }
        }
  // 如果剩余的bit不夠8個,最后一個byte剩余位為0
        if (bitIndex != 0) {
            currLine[currLineIndex++] = currValue;
        }
// 調用開源工具的方法對數據做后續處理
        consumer.consume(currLine, null);
    }
}

最終用修改后的開源PNG編碼器代替ImageIO.write方法,這里使用壓縮級別為1

byte[] result = new PngEncoder()
.withBufferedImage(image)
.withMultiThreadedCompressionEnabled(false)
// 配置壓縮級別為1
.withCompressionLevel(2)
// 設置位深度為1bit
.withBitDepth(1)
.toBytes();

最終經過優化后測試,和最開始測試時相比,PNG編碼步驟上,無論在耗時還是文件大小上都有很大改善

6 總結

通過對問題的優化,對以PNG為例的位圖文件結構,和Java中對圖片的基本操作有了漸進式的理解;同時也意識到,日常工作中,通過對業務本身的理解,清楚知道業務的邊界在那里,加上對技術基礎知識的深入理解,才能更細致地針對性做出優化。

作者:京東物流 馮凱

來源:京東云開發者社區 自猿其說Tech 轉載請注明來源

責任編輯:武曉燕 來源: 今日頭條
相關推薦

2019-01-21 11:17:13

CPU優化定位

2015-07-17 10:04:33

MKMapView優化

2010-04-01 22:16:21

2020-06-05 08:53:31

接口性能實踐

2011-02-22 09:29:23

jQueryJavaScript

2017-06-12 11:09:56

計數架構數據庫

2019-04-18 14:06:35

MySQL分庫分表數據庫

2020-09-23 06:52:49

代碼方法模式

2011-06-28 10:41:50

DBA

2022-09-15 10:02:58

測試軟件

2020-08-19 11:02:39

系統ssh登錄

2024-09-26 10:41:31

2021-03-18 23:47:18

MySQLselect索引

2021-01-08 13:52:15

Consul微服務服務注冊中心

2020-10-27 10:35:38

優化代碼項目

2018-05-25 14:41:56

Serverless無服務器構造

2017-12-07 12:47:48

Serverless架構基因

2015-07-14 10:34:42

ViewModel代碼高效

2021-12-27 10:08:16

Python編程語言

2020-10-24 13:50:59

Python編程語言
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 天天射美女 | 视频一区二区在线 | 欧美一区2区三区3区公司 | 中文字幕精品一区 | 欧美影院 | 在线成人免费观看 | 日韩一区二区三区在线看 | 日韩欧美在线视频 | 不卡在线视频 | 久久精品欧美一区二区三区不卡 | a黄在线观看| 免费能直接在线观看黄的视频 | 久久久99精品免费观看 | 免费在线日韩 | 亚洲 欧美 另类 日韩 | 一区二区免费看 | 国产精品国产精品国产专区不片 | 欧美性高潮 | 国产精品视频999 | 蜜桃传媒一区二区 | 日日摸夜夜添夜夜添精品视频 | 久久久久久久国产 | 久久国产精彩视频 | 欧美亚州综合 | 琪琪午夜伦伦电影福利片 | 在线中文字幕日韩 | 国产精品久久视频 | 国产一区二区高清在线 | 国产精品一级在线观看 | 91久久久久 | 99pao成人国产永久免费视频 | 亚洲精品在线看 | 91精品中文字幕一区二区三区 | a级网站| 一级做受毛片免费大片 | 91精品久久久久久久99 | 精品国产青草久久久久96 | 国产精品视频综合 | 久久免费高清视频 | 另类亚洲视频 | 国产精华一区 |