Auto Inverse PDF
介绍
本项目源于某一特殊情景——PDF打印
,笔者拿到了一份黑底白字的PDF资料:
而使用这个PDF文件直接进行打印将耗费大量油墨,故笔者想要获取一种可以反色打印PDF文件的方式。
经过大量查找之后发现,市面上有不少PDF阅读器具有反色功能,但是经过笔者测试:某些阅读器反色功能仅仅是显示反色,真正打印或者另存为时仍然会保持原色,不可否认的是某些场景下确实需要,但……这并非是笔者所需要的;而某些具有反色打印功能的阅读器要么存在广告,要么需要付费使用(甚至还会有P2P下崽器
)。
确定方法
由此,笔者萌发了自制反色输出程序,并大量查找资料。如资料1、资料2、资料3,大家见仁见智,有使用Python
的,有用Photoshop
的(毕竟PDF是Adobe
的),还有使用一些其他软件配合命令行的,玩法众多。而笔者更偏向于使用Python
:
- 轻量化 优于
Photoshop
- 简单化 优于附加下载其他软件
- 可控性强
所以笔者首先采用了资料1所使用的方法。
开始工作
根据资料作者所写,配置好环境,直接copy到文件里:
import datetime
import os
from PIL import Image
import PIL.ImageOps
import glob
import fitz
def pyMuPDF_fitz(pdfPath, imagePath):
startTime_pdf2img = datetime.datetime.now() # 开始时间
print("imagePath=" + imagePath)
pdfDoc = fitz.open(pdfPath) # 打开pdf
for pg in range(pdfDoc.pageCount):
page = pdfDoc[pg] #取出指定页
rotate = int(0) # 不进行旋转
# 每个尺寸的缩放系数为1.3,这将为我们生成分辨率提高2.6的图像。
# 此处若是不做设置,默认图片大小为:792X612, dpi=96
zoom_x = 5
zoom_y = zoom_x
mat = fitz.Matrix(zoom_x, zoom_y).preRotate(rotate)
pix = page.getPixmap(matrix=mat, alpha=False)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
img = PIL.ImageOps.invert(img)
if not os.path.exists(imagePath): # 判断存放图片的文件夹是否存在
os.makedirs(imagePath) # 若图片文件夹不存在就创建
img.save(imagePath + '/' + 'images_%s.png' % pg, "PNG")
print("<<转换进度>>\t"+str(100*pg/pdfDoc.pageCount)+"%", flush=True)
endTime_pdf2img = datetime.datetime.now() # 结束时间
print('pdf转换时间=', (endTime_pdf2img - startTime_pdf2img).seconds)
def pic2pdf(imagePath, newPath):
doc = fitz.open()
for img in sorted(glob.glob(imagePath + "/*")): # 读取图片,确保按文件名排序
print("添加页面"+str(img))
imgdoc = fitz.open(img) # 打开图片
pdfbytes = imgdoc.convertToPDF() # 使用图片创建单页的 PDF
imgpdf = fitz.open("pdf", pdfbytes)
doc.insertPDF(imgpdf) # 将当前页插入文档
if os.path.exists(newPath):
os.remove(newPath)
doc.save(newPath) # 保存pdf文件
doc.close()
if __name__ == "__main__":
pdfPath = "源文件.pdf"
imagePath = "中间文件路径"
newPath = "新文件.pdf"
pyMuPDF_fitz(pdfPath, imagePath)
pic2pdf(imagePath,newPath)
由于使用的文件页数超过10页
,出现了排序异常的情况:
根据资料的评论区,热心的大佬为该程序重新编写了排序函数:
def pic2pdf(imagePath, newPath):
doc = fitz.open()
imgs = len(glob.glob(imagePath + "/*"))
for img in range(imgs): # 读取图片,确保按文件名排序
img = imagePath + '/images_{}.png'.format(img)
print("添加页面"+str(img))
imgdoc = fitz.open(img) # 打开图片
pdfbytes = imgdoc.convertToPDF() # 使用图片创建单页的 PDF
imgpdf = fitz.open("pdf", pdfbytes)
doc.insertPDF(imgpdf) # 将当前页插入文档
if os.path.exists(newPath):
os.remove(newPath)
doc.save(newPath) # 保存pdf文件
doc.close()
重新运行之后排序正常了。
对了,这里不得不提一下,笔者参考的资料1所使用的模块——PyMuPDF
,根据其官方文档由于版本更新问题,修改了几乎全部的内置函数命名方式,由原来的驼峰式、混合式统一为蛇形:
The original naming convention for methods and properties has been “camelCase”. Since its creation around 2013, a tremendous increase of functionality has happened in PyMuPDF – and with it a corresponding increase in classes, methods and properties. In too many cases, this has led to non-intuitive, illogical and ugly names, difficult to memorize or guess.
A few versions ago, I therefore decided to shift gears and switch to a “snake_cased” naming standard. This was a major effort, which needed a step-wise approach. I think am done with it now (version 1.18.14).
这导致笔者在初次使用之时程序直接报错:
Traceback (most recent call last):
File "e:\Programs\Python\InversePDF\test.py", line 56, in <module>
pyMuPDF_fitz(pdfPath, imagePath)
File "e:\Programs\Python\InversePDF\test.py", line 17, in pyMuPDF_fitz
for pg in range(pdfDoc.pageCount):
AttributeError: 'Document' object has no attribute 'pageCount'
后来回退版本就好了,不过为了适配最新官方手册和新特性,我还是升级了原有的版本。
小问题
其实文章到此也应该结束了,但是笔者在实际使用中还是遇到了一个问题——输出的PDF分辨率不足。
于是笔者调整了zoom
参数,将输出PDF的分辨率调高,但这时一个重大问题发生了——程序变得异常耗时!
#zoom_x = zoom_y = 5
time=21
#zoom_x = zoom_y = 10
time=54
#zoom_x = zoom_y = 20
time=199
这引发了笔者极大的兴趣,这也是上文提到的,升级PyMuPDF
版本,查阅手册,开发!
笔者先是测试了各个步骤的耗时,发现耗时最多的函数分别是
#平均5s
pix = page.getPixmap(matrix=mat, alpha=False)
#平均3s
img.save(imagePath + '/' + 'images_%s.png' % pg, "PNG")
第一个函数无法避免的耗时,因为这个函数是模块内置函数,是将PDF提取成图像的必经之路。
而第二个函数也许还会有一线生机。可以看到这份程序实际的使用逻辑是这样的
读取PDF->
生成图片文件->
反色->
读入图片文件->
合并生成PDF
这个逻辑并无不可,但是过于浪费,中间的生成文件理论上应该不需要,因为具体的图像格式变换逻辑是:pixmap
->PIL.Image
->png
->pixmap
中间步骤png
略显多余,所以笔者在第一时间想要去除。
这又涉及到一个问题:
PIL.Image
如何转换为pixmap
,这一点官方手册并未直接给出,而是给出了numpy.array
->pixmap
import numpy as np
import fitz
#==============================================================================
# create a fun-colored width * height PNG with fitz and numpy
#==============================================================================
height = 150
width = 100
bild = np.ndarray((height, width, 3), dtype=np.uint8)
for i in range(height):
for j in range(width):
# one pixel (some fun coloring)
bild[i, j] = [(i+j)%256, i%256, j%256]
samples = bytearray(bild.tostring()) # get plain pixel data from numpy array
pix = fitz.Pixmap(fitz.csRGB, width, height, samples, alpha=False)
pix.save("test.png")
所以逻辑就变成了pixmap
->PIL.Image
->numpy.array
->pixmap
而原有的两个自定义函数也被笔者改为了一个自定义函数。
说到这里,应该放上一个速度对比截图的,但是很遗憾,由于笔者开发时不断发现PyMuPDF
模块的新特性并应用,导致笔者只有最终版的程序可以跑通。
首先是zoom
参数矩阵被替换为dpi
参数
#原程序(旧版本1.18.16)
rotate = int(0) # 不进行旋转
zoom_x = 5
zoom_y = zoom_x
mat = fitz.Matrix(zoom_x, zoom_y).preRotate(rotate)
pix = page.getPixmap(matrix=mat, alpha=False)
#新程序(新版本1.20.2)
pix = page.get_pixmap(dpi=600)
其次是一个重大改进:舍弃掉了PIL库,因为pixmap
数据类型自带反色函数invert_irect()
,并配合其他相关处理函数,整体操作逻辑变成了:
读取PDF->
生成图片->
反色->
合并生成PDF
格式变换逻辑变成了:
pdf
->pixmap
->pdf
(上面所讨论的变换逻辑统统失效)
最终,程序从原来复杂的 50+ 行,变成了现在的 20+ 行:
import datetime, os, fitz
def Inverse(pdfPath, newPath):
startTime_pdf2img = datetime.datetime.now() # 开始时间
pdfDoc = fitz.open(pdfPath) # 打开源PDF
outdoc = fitz.open() # 建立输出PDF
for page in pdfDoc:
pix = page.get_pixmap(dpi=600)
pix.invert_irect()
outdoc.new_page(pno=-1, width= page.rect.x1, height= page.rect.y1) # 输出文件新建页
outpage = outdoc[-1]
outpage.insert_image(outpage.rect,pixmap=pix)
endTime_pdf2img = datetime.datetime.now() # 结束时间
print('转换时间=', (endTime_pdf2img - startTime_pdf2img).seconds,'s')
if os.path.exists(newPath):
os.remove(newPath)
pdfDoc.close()
outdoc.save(newPath) # 保存pdf文件
outdoc.close()
if __name__ == "__main__":
pdfPath = "./PDF/陶瓷.pdf"
newPath = "./out/反色-陶瓷.pdf"
Inverse(pdfPath, newPath)
运行时间大幅缩短
dpi=600
time=24s
可根据需求修改dpi
参数以提高分辨率(当然了pixmap
带来的分辨率下滑是无法改变的,毕竟很多的PDF文件中的内容是矢量图形,笔者也在考虑是否应当将其改为svg
)
这里是ZnYC!非常感谢看到最后,我们会在这里不定期更新一些奇奇怪怪的东西,欢迎关注!
您的赞赏是我们前进的最大动力!!
↘点↓个↓赞↓吧↙!!