日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當前位置:首頁 > 科技  > 軟件

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

來源: 責編: 時間:2023-11-06 08:53:46 281觀看
導讀1 引言日常開發中接到這樣的需求,上游系統請求獲取一張A4單據用于倉庫打印及展示,要求PNG圖片格式,但是我們內部得到的單據格式為PDF,需要提取PDF文檔的元素并生成一張PNG圖片。目前已經有不少開源工具實現了這一功能,我們

1 引言

日常開發中接到這樣的需求,上游系統請求獲取一張A4單據用于倉庫打印及展示,要求PNG圖片格式,但是我們內部得到的單據格式為PDF,需要提取PDF文檔的元素并生成一張PNG圖片。目前已經有不少開源工具實現了這一功能,我們找了網上使用比較多的Apache PDFBox庫來實現功能,如下Tz628資訊網——每日最新資訊28at.com

// Step 1PDDocument document = PDDocument.load(content);PDFRenderer pdfRenderer = new PDFRenderer(document);// 獲取第1頁PDF文檔OutputStream os = new ByteArrayOutputStream()// Step 2// 為了保證圖片的清晰,這里采用600DPIBufferedImage image = pdfRenderer.renderImageWithDPI(0, 600);// Step 3ImageIO.write(image, "PNG", os);

實際測試時,明顯感覺到卡頓,當一次請求的單據數目較多時尤其嚴重。Tz628資訊網——每日最新資訊28at.com

經統計,各步驟本機單次運行耗時如下:Tz628資訊網——每日最新資訊28at.com

pdf 初始化(Step 1):2ms文檔提取及圖片繪制(Step 2):520ms圖片編碼 (Step 3):3823msTz628資訊網——每日最新資訊28at.com

我們發現,最后一句代碼耗時接近4秒,拖累了整體性能。我們要如何優化這樣一個問題呢?Tz628資訊網——每日最新資訊28at.com

2 BufferedImage介紹

在討論優化問題之前,首先要搞清楚待優化的代碼是做什么的。如上代碼中,使用renderImageWithDPI方法,將文檔元素繪制為BufferedImage對象。Tz628資訊網——每日最新資訊28at.com

Tz628資訊網——每日最新資訊28at.com

根據描述,BufferedImage用來描述一張圖片,其內部保存了圖片的顏色模型(ColorModel)及像素數據(Raster)。這里簡單解釋就是,內部的Raster實現類中,以某種數據結構(如Byte數組)表示圖片的所有像素數據,而ColorModel實現類,則提供了將每個像素的數據,轉換為對應RGB顏色的方式。Tz628資訊網——每日最新資訊28at.com

BufferedImage的構造函數中,可以傳入圖片類型來決定使用哪一種ColorModel和Raster。引言的示例中,PDFRender源碼中默認生成的圖片類型為 TYPE_INT_RGB,這種類型表示,每一個像素使用R、G、B三條數據表示,每條數據使用單字節(0~255)表示。Tz628資訊網——每日最新資訊28at.com

public BufferedImage(int width, int height, int imageType)

需要注意的是,BufferedImage并不表示某一張具體的位圖,而是通過描述每個像素的數據,抽象地表達一張圖片,因此,它可以在內存中通過操作像素數據,直接改變對應圖片。而通過ImageIO.write方法,可以將BufferedImage編碼為具體格式的圖片數據流。此方法會根據formatName選擇該文件格式的編碼器,來對BufferedImage內部的像素數據進行編碼。Tz628資訊網——每日最新資訊28at.com

public static boolean write(RenderedImage im, String formatName, OutputStream output) throws IOException

以下代碼為BufferedImage的簡單應用Tz628資訊網——每日最新資訊28at.com

將一個GIF圖片讀取到BufferedImage中,在坐標(10,10)位置打出ABC三個字符,并重新編碼成PNG圖片Tz628資訊網——每日最新資訊28at.com

BufferedImage image = ImageIO.read(new File("exmaple.gif"));image.getGraphics().drawString("ABC", 10, 10);ImageIO.write(image, "PNG", new FileOutputStream("result.png"));

下面這段代碼展示了另一類型的例子,它將圖片中所有的紅色像素點重置成黑色像素點Tz628資訊網——每日最新資訊28at.com

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模型中像素數據的存儲方式來決定數組下標。Tz628資訊網——每日最新資訊28at.com

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();}

那么,現在我們可以通過看源碼,了解引言的示例代碼的作用。Tz628資訊網——每日最新資訊28at.com

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

3 PNG文件格式淺析

根據上一節的內容可知,把BufferedImage編碼成PNG文件的過程,耗時接近2秒。我們需要簡單了解下編碼PNG文件的過程中,究竟在干什么。Tz628資訊網——每日最新資訊28at.com

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

PNG文件可以包含很多數據塊,最主要且必須包含的,是IHDR,IDAT及IEND三個數據塊Tz628資訊網——每日最新資訊28at.com

Tz628資訊網——每日最新資訊28at.com

我們通過十六進制打開PNG文件,就可以看到具體的數據塊分布Tz628資訊網——每日最新資訊28at.com

  • IEND
  • IEND為結束標志
  • IHDR

IHDR為文件頭,其后緊跟的字節描述了PNG文件的一些基礎屬性,如寬、高各占4各字節,而Color type和Bit Depth分別表示顏色類型和位深。Tz628資訊網——每日最新資訊28at.com

Tz628資訊網——每日最新資訊28at.com

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

Greyscale為灰度圖,每個像素用單一的灰度值來描述顏色,灰度值由0(白)到255(黑)逐步加深。Tz628資訊網——每日最新資訊28at.com

Truecolor即為一般的RGB三通道圖片,R、G、B每一個通道允許用8或16個比特來表示。Tz628資訊網——每日最新資訊28at.com

Indexed-color為索引色,需要配合調色板PLTE數據塊使用,這里不多做介紹。Tz628資訊網——每日最新資訊28at.com

后面兩種Greyscale with alpha, truecolor with alpha,顧名思義,即灰度和RGB圖像增加透明度通道Tz628資訊網——每日最新資訊28at.com

Tz628資訊網——每日最新資訊28at.com

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

比如在一張Colour type=Greyscale中,一個像素由1~255的灰度值來表示,那么這張圖片就是單通道8位深。Tz628資訊網——每日最新資訊28at.com

根據上表,我們知道位深度于顏色類型是有相關性的。比如Greyscale灰度圖只能支持1,2,4,8,16位深。Tz628資訊網——每日最新資訊28at.com

3.Compression Method壓縮算法

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

Tz628資訊網——每日最新資訊28at.com

LZ77算法可以設置一個壓縮級別參數,參數范圍為0 ~ 9,其中0為不壓縮;1為最快速度,但壓縮率較低;9為壓縮率最高,但速度會相對較慢。Tz628資訊網——每日最新資訊28at.com

4.Filter Method過濾方法

過濾方法即壓縮前的預處理,主要目的是對于一些顏色變化比較“陡”的圖片,通過一些數據的變換增加像素數據的重復度,從而增加壓縮率。Tz628資訊網——每日最新資訊28at.com

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

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

IDAT

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

4 優化方案

經過上述內容,針對引言中的問題,我們確定了2個優化方向Tz628資訊網——每日最新資訊28at.com

  • 業務上,無論怎樣的單據,都是要倉庫打印的,基本都是黑白圖片。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。Tz628資訊網——每日最新資訊28at.com

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

使用此方法后,ImageIO.write編碼過程耗時減少到150ms左右。Tz628資訊網——每日最新資訊28at.com

但是這樣改后,我們發現生成的PNG圖像,與原PDF文檔在觀感上相比,有一些發“虛”,如下圖Tz628資訊網——每日最新資訊28at.com

PDF截圖Tz628資訊網——每日最新資訊28at.com

Tz628資訊網——每日最新資訊28at.com

PNG截圖Tz628資訊網——每日最新資訊28at.com

Tz628資訊網——每日最新資訊28at.com

由于TYPE_BYTE_BINARY類型的BufferedImage每個像素只由0,1來表示黑白,很容易想到,這個現象的原因是出在判斷“多灰才算黑”上。Tz628資訊網——每日最新資訊28at.com

我們來看一下源碼中,BINARY類型BufferedImage的ColorModel,是如何判斷黑白的。BINARY類型的BufferedImage使用的實現類為IndexColorModel, 確定顏色的代碼段如下,最終由pix變量決定顏色的索引號。Tz628資訊網——每日最新資訊28at.com

int minDist = 256;int d;// 計算像素的灰度值int gray = (int) (red*77 + green*150 + blue*29 + 128)/256;// 在BINARY類型下,map_size = 2for (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的哪一個,來確定當前像素是黑是白。Tz628資訊網——每日最新資訊28at.com

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

對于當前業務場景,我們認為簡單地設置一個固定的閾值,來區分灰度值是一個適合的方式。Tz628資訊網——每日最新資訊28at.com

所以,為解決這個問題,我們設計了2種思路Tz628資訊網——每日最新資訊28at.com

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

從合理性上看,我認為1方案從程序結構角度是更合理的,但是實際應用中,卻選擇了方案2,理由如下Tz628資訊網——每日最新資訊28at.com

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

5 實際優化過程

我們通過網上搜到了開源Java實現的PNG編碼器pngencoder作為此業務場景下的編碼器。Tz628資訊網——每日最新資訊28at.com

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

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

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

/*** 在開源工具原有代碼基礎上,判斷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位深取數據方法Tz628資訊網——每日最新資訊28at.com

/*** 生成使用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方法,這里使用壓縮級別為1Tz628資訊網——每日最新資訊28at.com

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

最終經過優化后測試,和最開始測試時相比,PNG編碼步驟上,無論在耗時還是文件大小上都有很大改善Tz628資訊網——每日最新資訊28at.com

Tz628資訊網——每日最新資訊28at.com

6 總結

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

作者:京東物流 馮凱Tz628資訊網——每日最新資訊28at.com

來源:京東云開發者社區 自猿其說Tech 轉載請注明來源Tz628資訊網——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-17170-0.html一次單據圖片處理的優化實踐

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 如何使用圖算法,幫助我們理解和處理復雜的關系型數據

下一篇: 一篇文章學會用戶自定義消息及層次劃分

標簽:
  • 熱門焦點
  • 鴻蒙OS 4.0公測機型公布:甚至連nova6都支持

    華為全新的HarmonyOS 4.0操作系統將于今天下午正式登場,官方在發布會之前也已經正式給出了可升級的機型產品,這意味著這些機型會率先支持升級享用。這次的HarmonyOS 4.0支持
  • K60至尊版剛預熱 一加Ace2 Pro正面硬剛

    Redmi這邊剛如火如荼的宣傳了K60 Ultra的各種技術和硬件配置,作為競品的一加也坐不住了。一加中國區總裁李杰發布了兩條微博,表示在自家的一加Ace2上早就已經采用了和PixelWo
  • Mate60手機殼曝光 致敬自己的經典設計

    8月3日消息,今天下午博主數碼閑聊站帶來了華為Mate60的第三方手機殼圖,可以讓我們在真機發布之前看看這款華為全新旗艦的大致輪廓。從曝光的圖片看,Mate 60背后攝像頭面積依然
  • 女孩租房開2小時空調用完100元電費引熱議:5級能耗惹不起 月薪過萬電費也交不起

    近日,江蘇蘇州一女孩租房當天充值了100元電費,開著空調不到2小時發現電費已用完。對于為什么這個快,房東表示,電表壞了這種情況很多,之前也遇到過,給租客換
  • 0糖0卡0脂 旭日森林仙草烏龍茶優惠:15瓶到手29元

    旭日森林無糖仙草烏龍茶510ml*15瓶平時要賣為79.9元,今日下單領取50元優惠券,到手價為29.9元。產品規格:0糖0卡0脂,添加草本仙草汁,清涼爽口,富含茶多酚,保留
  • 只需五步,使用start.spring.io快速入門Spring編程

    步驟1打開https://start.spring.io/,按照屏幕截圖中的內容創建項目,添加 Spring Web 依賴項,并單擊“生成”按鈕下載 .zip 文件,為下一步做準備。請在進入步驟2之前進行解壓。圖
  • 19個 JavaScript 單行代碼技巧,讓你看起來像個專業人士

    今天這篇文章跟大家分享18個JS單行代碼,你只需花幾分鐘時間,即可幫助您了解一些您可能不知道的 JS 知識,如果您已經知道了,就當作復習一下,古人云,溫故而知新嘛。現在,我們就開始今
  • 虛擬鍵盤 API 的妙用

    你是否在遇到過這樣的問題:移動設備上有一個固定元素,當激活虛擬鍵盤時,該元素被隱藏在了鍵盤下方?多年來,這一直是 Web 上的默認行為,在本文中,我們將探討這個問題、為什么會發生
  • 當家的盒馬,加速謀生

    來源 | 價值星球Planet作者 | 歸去來自己&ldquo;當家&rdquo;的盒馬,開始加速謀生了。據盒馬官微消息,盒馬計劃今年開放生鮮供應鏈,將其生鮮商品送往食堂。目前,盒馬在上海已經與
Top 主站蜘蛛池模板: 华容县| 三门县| 宁远县| 山丹县| 磐石市| 桑植县| 延庆县| 南漳县| 喀喇沁旗| 板桥市| 崇阳县| 循化| 水富县| 微山县| 兴文县| 武清区| 崇义县| 拉萨市| 汉沽区| 平谷区| 枞阳县| 上高县| 正镶白旗| 临安市| 婺源县| 忻州市| 大兴区| 揭阳市| 京山县| 新营市| 平罗县| 灵台县| 伊金霍洛旗| 大关县| 天等县| 长乐市| 武威市| 岳西县| 钟山县| 香港 | 屯留县|