如果現在要寫一個將pdf文件的全部內容轉為一張png圖片的Python短程序(Python script),那麼最簡單的做法就是建立一個.py文件,然後在裏面直接將代碼按順序地寫出來。例如,我們現在使用PyMuPDF這個package去實現以上的功能,那先安裝PyMuPDF(以下命令行中的python和pip命令在不同操作系統可能有不同的寫法,本文略過不提):
$ pip install PyMuPDF
接著就可以編寫Python程序:
# 導入PyMuPDF包
import fitz
pdf_name = 'abc.pdf'
doc = fitz.open(pdf_name)
# ...
# 中間代碼暫時略過不寫
# 輸出成PNG文件
output_pix.writePNG('abc.png')
寫完後,就可以直接運行pdf_to_png.py程序,將abc.pdf轉為abc.png:
$ python pdf_to_png.py
這樣寫程序不是不行,但會有幾個問題。首先,如果你要在其他程序中將pdf文件轉為png圖片,難道要將這段代碼複製貼上到那個程序中嗎?如果有十個程序要用這個功能,難道又要將這段代碼複製貼上十次?就算可以這樣做,那以後要修改將pdf轉png這個程序,如發現有bug了,該怎麼辦?為了令代碼有重用性,可以將相關的程序改寫為一個函數:
import fitz
def convert_to_png(source, output, zoom=2.0):
"""
這是docstring的部份
"""
doc = fitz.open(source)
# 略
這樣改寫後,將要轉換的pdf文件和輸出的png圖片路徑作為函數convert_to_png
的參數source
和output
。既然為了重用而寫成函數,那就不要將文件的路徑硬性地寫入函數中,而應該讓使用者自己設定。另外,函數中還有一個keyword參數zoom
,用作調整輸出圖片的大小,預設值為2.0。
寫函數時,一般會在定義後,即函數的本體一開始加上名為「docstring」的文字,所以描述這個函數的用法和功能。加上docstring後,即可調用函數object的__doc__屬性或用help()函數顯示一個函數的docstring,作為一種查詢函數用法的做法:
>>> import pdf2png
>>> dir(pdf2png.convert_to_png)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__',
'__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> print(pdf2png.convert_to_png.__doc__)
這是docstring的部份
>>> help(pdf2png.convert_to_png)
Help on function convert_to_png in module pdf2png:
convert_to_png(source, output, zoom=2.0)
這是docstring的部份
上方先進入Python的interpreter,然後導入剛才寫的module pdf2png
。由於導入的法方是import pdf2png
,所以使用函數convert_to_png()
時,要把module的名字也寫上,即pdf2png.convert_to_png()
,這樣做就可以通過導入module的方法在其他程序中重用這個pdf轉png的函數。接下來我們用dir()
函數顯示這個函數object的全部屬性和方法,見到__doc__
屬性也在裏面,所以接下來就分別用__doc__
和help()
顯示convert_to_png()
的docstring。
要注意此時pdf2png
這個module中只有一個函數convert_to_png()
,並沒有任何代碼調用這個函數,那可不可以在函數convert_to_png()
之後加入代碼去調用這個函數,以做到將abc.pdf轉為abc.png的效果呢?例如:
import fitz
def convert_to_png(source, output, zoom=2.0):
"""
這是docstring的部份
"""
doc = fitz.open(source)
# ...
convert_to_png('abc.pdf', 'test_dir/abc.png')
這樣做時,每次在terminal中運行pdf2png.py,如:
$ python pdf2png.py
就會將abc.pdf轉為abc.png,並放入test_dir資料夾中(假設test_dir這個資料夾存在)。不過,這樣做又有問題出現:
- 如何在不修改pdf2png.py代碼的情況下轉換其他的pdf文件?
- 如果在其他程序中
import pdf2png
,又會有甚麼事情發生?
我們首先試試在其他程序中import pdf2png
,看看會出現甚麼結果。為簡單,將pdf2png.py中的代碼改為只剩一行print
函數輸出一些東西:
import fitz
def convert_to_png(source, output, zoom=2.0):
# ...
print(f'Converting {source} to {output}...')
convert_to_png('abc.pdf', 'test_dir/abc.png')
進入Python interpreter並導入pdf2png
這個module:
$ python
Python 3.8.2 (default, Mar 20 2020, 17:41:36)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pdf2png
Converting abc.pdf to test_dir/abc.png...
從上面的結果可見,當執行了import pdf2png
後,pdf2png.py中的最後一行convert_to_png('abc.pdf', 'test_dir/abc.png')
亦被執行。這代表任何其他程序只要導入了這個module,都會執行一次convert_to_png('abc.pdf', 'test_dir/abc.png')
這句代碼,將abc.pdf轉為abc.png並放在test_dir裏。這顯然不是我們想要的,我們只想導入pdf2png
這個module後使用其中的convert_to_png()
函數 ,並不希望執行裏面的其他內容。
有沒有辦法可以讓Python知道,如果將module導入時,就不執行某些代碼,而運行module時,就會執行其中的全部代碼,令到一個module既可以用於導入到其他程序,自己本身又可以運行?解決方法是使用Python中一個特別的變量:__name__
。請看以下例子:
print(__name__)
這個程序將變量__name__
顯示出來,那麼它的結果是甚麼呢?如果運行test_name.py時,有:
$ python test_name.py
__main__
即__name__
等於__main__
。當導入test_name
時,又有:
$ python
Python 3.8.2 (default, Mar 20 2020, 17:41:36)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import test_name
test_name
即__name__
等於module的名字test_name
。可見,__name__
這個變量是會變化的,只要利用好這一點,就能夠讓Python只在module運行時才執行某些代碼。所以,pdf2png.py的寫法可改為:
import fitz
def convert_to_png(source, output, zoom=2.0):
# ...
print(f'Converting {source} to {output}...')
if __name__ == '__main__':
convert_to_png('abc.pdf', 'test_dir/abc.png')
當導入這個module時,由於__name__
等於module的名字pdf2png
,所以上方第7行之後的代碼不會執行,但其中的函數卻可以使用:
$ python
Python 3.8.2 (default, Mar 20 2020, 17:41:36)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pdf2png
>>> pdf2png.convert_to_png('abc.pdf', 'test_dir/abc.png')
Converting abc.pdf to test_dir/abc.png...
解決導入的問題了,現在要解決另一個問題,即如何在不修改pdf2png.py代碼的情況下運行pdf2png.py來轉換其他的pdf文件。要做到這一點,就要給程序傳入至少兩個值:待轉換的pdf文件位置及輸出的png圖片位置。將pdf2png.py修改為:
import fitz
def convert_to_png(source, output, zoom=2.0):
# ...
print(f'Converting {source} to {output}...')
if __name__ == '__main__':
import sys
print(sys.argv)
# convert_to_png('abc.pdf', 'test_dir/abc.png')
在terminal這樣運行pdf2png.py,並加入三個參數a、b和c:
$ python pdf2png.py a b c
['pdf2png.py', 'a', 'b', 'c']
Converting abc.pdf to test_dir/abc.png...
可見,sys
module中的sys.argv
是一個list,sys.argv[0]
為Python程序的名字,後面是接下來的命令行參數。利用這一點,我們就可以為pdf2png.py傳入其他的pdf文件位置和設定png圖片的輸出位置。將pdf2png.py修改為:
import fitz
def convert_to_png(source, output, zoom=2.0):
# ...
print(f'Converting {source} to {output}...')
if __name__ == '__main__':
import sys
convert_to_png(sys.argv[1], sys.argv[2])
於是運行時加入適當的參數,就可以自定要轉換的文件位置啦:
$ python pdf2png.py xyz.pdf test_dir/xyz.png
Converting xyz.pdf to test_dir/xyz.png...
寫到這裏,我們總結一下Python短程序的一般格式:
- 將程序的主要功能用函數包裝起來。
- 為函數加入docstring。
- 用
if __name__ == '__main__'
讓程序既可被導入,又可以直接運行。 - 使用
sys.argv
取得命令行傳入的參數,讓程序在直接運行時使用。
知道Python短程序的一般格式後,剩下來就是將程序的功能加以實現:
import fitz
def convert_to_png(source, output, zoom=2.0):
"""
This function converts a multipage pdf file to a single png image.
Args:
source: (str) path of the pdf file.
output: (str) path of the output png file.
zoom: (float) zoom factor, the default value is 2.0.
Returns:
None
"""
doc = fitz.open(source)
if not output.lower().endswith('.png'):
print(f"{output} is not an image name.")
return
mat = fitz.Matrix(zoom, zoom)
images = []
for page in doc:
pix = page.getPixmap(matrix=mat, alpha=False)
images.append(pix)
out_width = images[0].width
out_height = images[0].height * doc.pageCount
out_pix = fitz.Pixmap(images[0].colorspace,
(0, 0, out_width, out_height), images[0].alpha)
for i in range(doc.pageCount):
images[i].y = images[i].height * i
out_pix.copyPixmap(images[i], images[i].irect)
print(f"Converting {source} to {output}...")
out_pix.writePNG(output)
if __name__ == '__main__':
import sys
if len(sys.argv) < 3:
sys.exit(
f"USAGE: python {sys.argv[0]} [pdf path] [output png path] [zoom level]")
else:
convert_to_png(sys.argv[1], sys.argv[2],
sys.argv[3] if len(sys.argv) > 3 else 2.0)
上述代碼主要應用PyMuPDF這個package,其中已經能夠檢測許多不同類型的錯誤,如輸入的檔案類型不是pdf、輸出位置的資料夾不存在等,所以我們在第47行就直接調用函數而沒有做其他錯誤檢測。這個程序還有許多未完善的地方,不過本文的主題不是關於如何將pdf文件轉為png圖片,只是借這個內容說明Python短程序的一般格式是甚麼,所以在此就不再寫得更詳細了。如對PyMuPDF有興趣,可看官方的文檔。
他和Python有關的文章:Python中用os.walk()列出資料夾中的全部內容。