这是一个用来压缩扫描出的PDF文档的python小脚本。压缩成纯黑白(1bit色深)的效果最好。
这里权作自己的思路笔记了~
用fitz
包打开PDF并提取每页的图片,然后用PIL
和/或cv2
包处理图片,然后用img2pdf
包重新合成成pdf。属实是十分trivial的功能。
有两种方法:一种是直接用.get_page_images()
得到pdf内部的原始图片数据并直接输出,该命令得到的是一个记录了每张图片的信息的数组,每条信息也是一个数组,其首个元素就是该图片的引用号xref
,通过该引用号即可从pdf文件中调取出原始图片。如果每页只是纯粹的一张图片的话是最好的,但是如果不是(例如,用某些手机app生成的扫描pdf会加上水印、二维码之类的)就会掉出一大堆图片,难以处理:
def extract_pic1(pdf_in,current_page):
for img in pdf_in.get_page_images(current_page):
xref=img[0]
fitz.Pixmap(pdf_in, xref).save("./RAW/%04d.png"%(current_page+1))
另一种方法就是用.get_pixmap()
把整个页面转化为图片,这样虽然损失了图片的原始信息,但是是万无一失的。可以通过控制dpi来控制生成的图片的大小,比如下面控制为宽度2000:
def extract_pic2(pdf_in,current_page):
page = pdf_in.load_page(current_page)
page_width = page.rect.width # in px, 1px = 1/72 inch
page.get_pixmap(dpi=int(2000*72/page_width)).save("./RAW/"+"%04d.png"%(current_page+1))
这里要用到PIL
和cv2
包两个包的功能:
cv2
包里有一个非常好用的功能:动态阈值cv2.adaptiveThreshold()
,也就是在二值化的时候,是将图片划定为许多个小方格,在每个小方格里单独确定阈值,使每个小方格里黑:白比例最接近于1。这样可以有效避免在转纯黑白时,画面出现大片死黑/死白的情形,并且也无需费心手动调节阈值了。- 纯黑白的图片用tiff格式、CCITT group 4压缩方法储存的压缩效率最高,一张10M像素的黑白扫描图仅需几十kB,用
PIL
包这样保存最方便,注意提前将色域转为1 bit。
另外,鉴于转纯黑白时字体边缘的过渡将变得十分生硬,所以宜在转换前适当放大图片,最终文件大小只会有很小的变大。
def toBW(file_name):
img_cv=cv2.imread("./RAW/"+file_name)
height, width, channels = img_cv.shape
if width<3000:
img_cv=cv2.resize(img_cv,(3000,round(height*3000/width)),interpolation=cv2.INTER_LANCZOS4) # 放大到宽度3000
gray=cv2.cvtColor(img_cv ,cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 81, 30) # 二值化
img_pil=Image.fromarray(binary) # 从cv2的格式转到PIL的格式
imgdata=img_pil.convert('1') # 色域转为1 bit
dpi_set=imgdata.width/7.09 #固定页面宽度为18cm(7.09inch)的话,dpi应该就是img.width/7.09
imgdata.save("./RAW/alter/"+file_name+".tiff", format="TIFF", compression="group4",dpi=(dpi_set,dpi_set))
没什么好说的,用jpg格式,设一个较低的quality
、降采样到4:2:0
即可。另外之前输出图片的时候把页面图片宽度控制为了2000,这是偏大的,注意给它降回去,否则达不到压缩的目的。
def toColor(file_name,set_quality):
imgdata=Image.open("./RAW/"+file_name)
set_width=1200
im_resized=imgdata.resize((set_width,int(set_width*imgdata.height/imgdata.width)))
dpi_set=im_resized.width/7.09 #固定页面宽度为18cm(7.09inch)的话,dpi应该就是img.width/7.09
im_resized.save("./RAW/alter/"+file_name, optimize=True, quality=set_quality,subsampling=2, dpi=(dpi_set,dpi_set))
img2pdf
包操作是最方便的,压缩效率也是最高的:
with open(new_file, "wb") as f:
f.write(img2pdf.convert(glob.glob("./RAW/alter/*.*")))
但是这个包在处理tiff文件时候每个文件都会弹出一行提示,几百行很烦人!我之前改了该包的源代码把这个提示干掉了,但是现在忘了在哪里改的了,等我记起来了再在这里记录罢。
压缩pdf的时候会出现只选取一些页面保留彩色、其它页面变纯黑白的情况。这里仿照通用的选页语法写了个小函数,可以将输入的形如
1-3,15,20-22
的文本转化为页码数组
[1,2,3,15,20,21,22]
用了两次.split()
方法而已:
def getPages(str):
pages_cache=str.split(',')
the_pages=[]
for item in pages_cache:
item_cache=item.split('-')
if len(item_cache)==2: # 此时item_cache形如['20','22']
the_pages.extend(range(int(item_cache[0]),int(item_cache[1])+1))
else: # 此时item_cache形如['15']
the_pages.append(int(item_cache[0]))
return the_pages