利用命令列將pdf轉換為長圖

__main__發表於2018-02-27

利用命令列將pdf轉換為長圖

業務場景

專案中會時不時遇到展示pdf檔案的需求,比如需要展示某些合同或者一些PPT報告之類的。我們在做《娛樂寶》、《票票專業版》專案期間都遇到了這樣的需求。針對如何展現pdf檔案的內容,一般無外乎以下幾種方案:

  • 讓客戶端渲染pdf
  • H5頁面通過開源的JS庫對PDF檔案進行渲染
  • 將pdf列印為圖片,然後再利用H5頁面對檔案中的多張圖片分步下載並渲染。

而這幾種方案在實際操作過程中又分別有各自的問題,我們首先討論第一種方案。讓客戶端渲染PDF有兩種方式:1. 藉助系統已有的功能,比如webbiew。2. 利用開源的pdf渲染庫。iOS中webview自帶了pdf渲染功能,同時支援縮放等操作,體驗相當好,但安卓不支援,需要自己開發實現。而第三方PDF渲染庫普遍比較大,一般都要好幾兆,為了這樣一個非核心功能引入這麼大一個庫,客戶端同學是堅決不會答應的。第二種方案初看起來不錯,至少省去了客戶端相容的成本。調研了下JS渲染PDF方面的實現,比較著名的是Mozzilapdf.js。但這個庫也有一些問題:

  • 這個庫的原始檔體積不小,原始檔282K,gzip壓縮後110K。
  • 需用通過Ajax方式載入PDF檔案,而正式專案中,我們一般需要把PDF檔案上傳到CDN。
  • 使用Canvas對PDF中的圖片進行渲染,PDF中圖片比較多的話將會生成大量的canvas
  • 渲染出的結果存在相容性問題,不同字型設定會導致渲染結果差異很大。

前兩個問題通過一些技術手段還能繞過去,但後兩個幾乎無解了,尤其是用canvas渲染圖片。canvas在移動端太耗效能了,Canvas太多的話會造成瀏覽器渲染效能嚴重下降,iphone下甚至會導致APP崩潰。採用Canvas渲染圖片的證據。

不同字型設定導致渲染結果不一致的問題:

此外在測試過程中還發現:那個庫提供的Demo頁面在UC下開啟且開啟的PDF檔案比較大的情況下,多翻幾頁之後會出現頁面載入不出來的情況,因此這個庫在H5下的相容問題堪憂。 因此採用JS庫渲染PDF目前來看不太適合應用在H5專案中,在PC專案中還可以考慮下。

經過排除後目前只剩下將PDF轉換為圖片然後用H5來渲染這一方案了,至於此方案的具體實現,可以參照之前釋出的一篇文章《一個簡單H5活動頁面模板的設計》。這個方案比較簡單可靠,但面臨一個很煩人的問題:需要將PDF的每一頁轉換為圖片然後拼接為長圖。如果這個過程需要人工來完成將是非常繁瑣的,而如果檔案比較大的話那簡直是噩夢了,因此這個過程是必須由程式自動來完成的。

利用命令將PDF自動轉換為長圖

如果由程式完成將PDF轉換為長圖,必須要實現兩個功能:

  • 將PDF的每一頁轉換為圖片
  • 將轉換後的多張圖片合併為一張長圖

還好這兩個功能都有相應的軟體支援,而且這兩個軟體的命令列支援都非常好,而且都支援brew進行安裝。將PDF轉換為圖片最著名的庫莫過於GhostScript,在專案中我們也選用了這個庫。將PDF的每一頁轉換為圖片可以通過下面的命令來實現

gs -sDEVICE=pngalpha                       # 輸出格式為png
    -o "./tmp-pdf-page/$filename-%d.png"   # 設定每一頁對應圖片的名稱
    -r144 "$pdfname";                       # 設定每英寸內的畫素數

將多張圖片拼合為一張有多種軟體可以實現,比較有名的是ImageMagickGraphicsMagick。ImageMagick資歷最老,文件最全,支援的特性最多,但執行起來比較緩慢。GraphicsMagick脫胎於ImageMagick 5.x,支援的特性比較少,命令格式幾乎與ImageMagick通用,執行速度飛快,但文件非常少,而且有些特性不支援(本文後面程式中所使用的切功能:shave在測試時沒有除錯通過)。考慮到這個功能無論在本機還是服務端呼叫都不是很頻繁,因此我們使用了ImageMagick。下面的程式碼可以實現將多張圖片拼接為一張長圖,為了輸出的圖片更加美觀,圖片之間新增了一定的白色空白。

convert ./tmp-pdf-page/$filename-*.png 
    -background  white  
    -bordercolor white     # 設定圖片邊框顏色
    -border  0x50          # 圖片上下新增50畫素邊框 ,因此圖片之間有100px的邊框
    -append                # 圖片直接垂直拼接,如果水平拼接可用+append
    -shave   0x50          # 刪除合併後圖片的上下邊框,GraphicsMagick不支援此操作
    -resize  1080          # 將拼接後的圖片寬度調整為1080
    -quality 85            # 設定輸出的JPG圖片質量
    -sharpen 0x1.0         # 拼接後的圖片字型有點發虛,在垂直方向做銳化處理 
$filename-dest.jpg

有一點需要說明下,安裝GhostScript後,ImageMagick內部可以直接呼叫GhostScript實現將PDF轉換為長圖,具體實現可以參考如下:

  convert demo.pdf 
     -resize         620              # 設定每張圖片尺寸
     -alpha         remove      
     -density       620              # 設定解析度,按文件應該越高越好 
     -mattecolor    `#cccccc`    # 設定間隔顏色,作用與上面程式碼中的border相同
     -frame         10x5             # 設定圖片間隔寬度
     -append 
     -quality       85 
     -frame         0x5 
     -sharpen       0x1.0 
 demo.jpg

這段程式碼省去了第一步利用GhostScript將PDF轉換為多張圖片的步驟,但效果不是很理想,無論怎麼設定解析度(density)和JPG質量(quality),轉換出來的圖片都有點糊,因此實際專案中我們使用了分開處理的方案。

因為操作比較多,我們寫了個bash指令碼對這些邏輯做封裝,使用方式為:bash convert.sh demo.pdf,指令碼完整程式碼如下:


#!/bin/bash

## 計算pdf檔名,參考資料:
# http://www.runoob.com/linux/linux-shell-variable.html
# https://stackoverflow.com/questions/965053/extract-filename-and-extension-in-bash/965072
# https://stackoverflow.com/questions/965053/extract-filename-and-extension-in-bash
# https://stackoverflow.com/questions/3362920/get-just-the-filename-from-a-path-in-a-bash-script
# 

pdfname=$1
filename="${pdfname%%.*}"

## 建立臨時資料夾儲存每張pdf頁面對應的圖片
mkdir tmp-pdf-page

## 將pdf轉換為多張png
gs -sDEVICE=pngalpha -o "./tmp-pdf-page/$filename-%d.png" -r144 "$pdfname";

## 將多張圖片合併為一張,每張圖片直接新增50畫素間隔,最後
# 將圖片尺寸設定1080寬度後裁掉第一張和最後一張的邊框,並
# 進行銳化處理後輸出為jpg。
# 參考資料:
#   http://www.imagemagick.org/Usage/crop/#border
#   http://www.imagemagick.org/Usage/crop/#frame

convert ./tmp-pdf-page/$filename-*.png 
    -background  white  
    -bordercolor white  
    -border  0x50     
    -append        
    -shave   0x50  
    -resize  1080  
    -quality 85    
    -sharpen 0x1.0 
$filename-dest.jpg

## 刪除單張pdf檔案對應的圖片
rm -rf ./tmp-pdf-page

轉換後圖片線上上的實際效果

是否可以應用到服務端?

答案是可以,這一方案依賴的兩個軟體:ImageMagick和GhostScript在Linux和Mac下均有提供,所以可以無縫移植的服務端。最早做這個方案的研究是在一年多以前,當時在做《娛樂寶》專案,每個專案上線都要上傳合同,所以把生成圖片並上傳CDN的功能做到了小二後臺中。當時是直接利用ImageMagick將PDF轉換為長圖的功能,沒有使用先用GhostScript轉換為多圖然後再用ImageMagick拼接的方案。當時的效果不是很理想,文字總是比較糊。但當時一來沒有找到理想的解決方案,二來支付寶對於圖片的大小有要求,所以就將就著用了。後來專案中又遇到了這個需求,所以花了些時間整理和優化了下,所以有了本文提到的這個方案。

移植到服務端沒有問題,但有幾點需要注意下:

  • 服務端環境一般都沒有安裝ImageMagick,需要自己手動安裝。而且Linux版本的ImageMagick處於安全考慮是不能直接完成pdf轉圖片的,需要對配置檔案進行一些配置。具體配置很簡單,基本看一眼就懂了。
  • Linux環境下中文字型普遍比較少,好像只有宋體,所以轉換出來的效果沒有Mac下好看。如果這種需求的頻率比較低且對最終的轉化效果由一些要求,建議還是在Mac下進行轉換。
  • 本文的bash指令碼方案會產生臨時檔案,不建議部署到服務端!

後記

目前這個方案還是不是特別理想,一個讓人很不爽的地方是:因為每個pdf頁面都需要生成一張圖片,所以程式執行期間需要建立多個臨時檔案。我一向對臨時檔案深惡痛絕,因為臨時檔案不僅會憑空增加磁碟訪問量,而且如果管理不好的話會造成垃圾檔案越堆越多,而如果不巧這個程式執行在服務端那就有可能把磁碟都佔滿了。在寫此文之前,我曾嘗試了多個方法把這個臨時檔案幹掉,但最終都不是很理想。

首先GhostScript提供將結果輸出到標準IO的功能,但ImageMagick的append功能無法支援從標準IO讀取多張圖片檔案,因此此方案行不通。GraphicsMagick也不支援從命令列讀取多張方案,但gm與GhostScript協同呼叫的效果比ImageMagick的效果要好,轉換後的效果與本文中用兩部實現的效果相當,但需要自己手動計算PDF頁數,而且因為不支援-shave引數,需要自己手動對最後轉換後的圖片進行必要的裁切。我們的使用場景主要是開發本機呼叫,開發時間所限,沒有對GraphicsMagick方案進行進一步調研。如果是部署到服務端,建議使用GraphicsMagick,不僅效率高而且不會產生臨時檔案,GraphicsMagick直接將PDF轉換為長圖的程式碼:

gm convert -density 1080 
     -mattecolor red    
     -frame      0x50   
     -append            
     -shave      0x50       # 裁剪功能在GM下沒有生效,不知是否是使用不當還是這種情況下不支援。
     -resize     1080      
     -quality    85     
     test.pdf[1-4]          # 這裡需要手動制定要轉換的頁碼範圍
     test-tmp.jpg

參考資料


相關文章