Вставить подпись в pdf или как спасти деревья

от автора

В век перехода к цифровому документообороту появляются курьёзные случаи когда цифровизация вроде есть, а вроде и нет. Одним из таких случаев оказалась ситуация, когда сотрудники распечатывали договор, присланный на электронную почту, ставили на распечатке факсимиле или печать, затем сканировали и отправляли обратно.

Исправить данное недоразумение, мне представляется возможным двумя путями: переходом на цифровые подписи, что потребует изменений в ведении документооборота у обоих сторон, либо программной вставкой изображения печати. Ввиду невозможности влиять на документооборот клиентов пришлось использовать второй путь, программной вставки изображения в документ.

Существует множество программ для работы с pdf, но вставка изображений в них либо платная, либо лимитированная. Текущая же задача требует безлимитной возможности редактирования документов и максимально простого интерфейса, чтобы программой мог сходу пользоваться любой человек без какого-либо обучения.

Таким образом я решил написать свое приложение для вставки изображений в pdf, отвечающего всем указанным выше требованиям. А так-как размер приложения и скорость работы (в пределах разумного!) не являются ключевыми, мне представилось оптимальным написать приложение на python после чего завернуть его в исполняемый файл.

Итак, приложение. Для создания графического интерфейса использовался модуль tkinter, так-как он осваивается «на лету», а внешний вид приложения был пожертвован в угоду скорости разработки. Таким образом получилось нечто такое:

Окно состоит всего из двух основных элементов: меню с кнопками и холста на котором будет размещено изображение документа. Так-как холст не может отображать pdf, для начала документ необходимо конвертировать в объект изображения. Для этих целей удобно использовать обертку над библиотекой poppler — pdf2image, которая имеет команду convert_from_path получающая путь к pdf файлу и возвращающая объект изображения. Далее, для удобства использования, изображение сжимается для размера холста (я выбрал размер 768*768 пикселей) по формуле коэф. масштабирования = размер холста / max(длина изображения, ширина изображения). После чего на холст добавляется изображение печати, которое можно перетаскивать по холсту. Таким образом получилось следующая картина:

Теперь переходим к сохранению готового документа. Изначально была идея просто вставить картинку в исходный pdf файл и для этих целей был найден модуль reportlab, но в ходе экспериментов с ним выяснилось, что pdf файлы имеют несколько иную координатную сетку, начинающуюся с левого нижнего угла, но при этом, некоторые документы имели сетку с начало в левом верхнем углу. Чтобы глубоко не вникать в особенности реализации pdf файлов, было решено просто конвертировать изображение обратно в pdf, благо это умеет делать модуль PIL, который уже использовался, для масштабирования изображений ранее. В остальном сохранение происходит по следующему сценарию: берется исходное изображение (не масштабированное), с помощью функции tkinter-а ‘coord’ находятся текущее координаты печати, координаты умножаются на коэф. масштабирования и печать размещается на документе (функция paste класса PIL Image). Таким образом документы не теряют в качестве и ни в чем не уступают отсканированным.

На этом этапе приложение было готово к работе, но возникала проблема с отсутствием python на пользовательских компьютерах. Для решения этой задачи использовался pyinstaller, который заворачивает код и интерпретатор python в один исполняемый файл. Здесь возникает только один нюанс: так-как приложение для открытия pdf требует установленной библиотеки poppler, нужно либо упаковать библиотеку внутрь exe файла, либо положить рядом с exe файлом. И в первом и во втором случае если собирать приложение с командой -noconsole путь до библиотеки не находится, так что пришлось оставить висящее окошко консоли при работе с приложением. На этом все, код приложения:

from tkinter import * from tkinter import filedialog from PIL import ImageTk, Image from pathlib import Path from pdf2image import convert_from_path import os  canvas_size = 768 document_type = (("document file", "*.jpg *.jpeg *.pdf"),                  ("pdf files", "*.pdf"), ("image files", "*.jpg *.jpeg")) sign_type = (("stamp file","*.png"),)  class DocCanv(Canvas): 	#Document 	DocumentList=None 	DocumentImage = None 	DocResize = 1 	DocImgLink = None 	CurentPage=0  	#Signature 	SignImage = None 	SignResize = DocResize 	SignImgLink = None 	SignObj = None   	def DocFile(self, use_in_func=False): 		if use_in_func is False: 			doc_path = filedialog.askopenfilename(filetypes=document_type) 			if (Path(doc_path).suffix).lower() == '.pdf': 				try: 					#try to use poppler from pyinstaller bundle temp directory 					self.DocumentList=convert_from_path(doc_path, poppler_path = os.path.join(sys._MEIPASS, "poppler") ) 				except: 					#reserve for poppler 					self.DocumentList=convert_from_path(doc_path, poppler_path = "poppler" ) 				self.DocumentImage=self.DocumentList[0] 			else: 				self.DocumentImage = Image.open(doc_path) 				self.DocumentList = [self.DocumentImage] 		(width, height) = self.DocumentImage.size 		self.DocResize = canvas_size / max(height, width) 		self.DocImgLink=ImageTk.PhotoImage(            self.DocumentImage.resize((int(width * self.DocResize), int(height * self.DocResize)), Image.ANTIALIAS)) 		self.create_image(0, 0, image=self.DocImgLink, anchor=NW)  	def SignFile(self, sign_path=None): 		if self.SignImage is not None: 			self.MergeFile() 			self.DocFile(True) 		if sign_path is None: 			sign_path = filedialog.askopenfilename(filetypes = sign_type) 		self.SignImage = Image.open(sign_path) 		(width, height) = self.SignImage.size 		self.SignResize=self.DocResize 		self.SignImgLink=ImageTk.PhotoImage(            self.SignImage.resize((int(width * self.SignResize), int(height * self.SignResize)), Image.ANTIALIAS)) 		self.SignObj = self.create_image(0, 0, image=self.SignImgLink, anchor=NW)  	def MoveSign(self, event): 		self.coords(self.SignObj, event.x, event.y)  	def ResizeSign(self, event): 		if event.delta > 0: 			self.SignResize = self.SignResize + 0.1 		else: 			self.SignResize = self.SignResize - 0.1  		(width, height) = self.SignImage.size 		self.SignImage.resize((int(width * self.SignResize), int(height * self.SignResize)), Image.ANTIALIAS) 		self.SignImgLink=ImageTk.PhotoImage(            self.SignImage.resize((int(width * self.SignResize), int(height * self.SignResize)), Image.ANTIALIAS) ) 		x, y = self.coords(self.SignObj) 		self.SignObj = self.create_image(x, y, image=self.SignImgLink, anchor=NW)  	def MergeFile(self): 		sign_coords =self.coords(self.SignObj) 		sign_coords = [(int)(x / self.DocResize) for x in sign_coords] 		(width, height) = self.SignImage.size 		width=int((width * self.SignResize)/self.DocResize) 		height=int((height * self.SignResize) / self.DocResize) 		ResizedSign=self.SignImage.resize((width,height), Image.ANTIALIAS) 		self.DocumentImage.paste(ResizedSign, box=sign_coords , mask=ResizedSign.convert('RGBA'))   	def SaveFile(self,f_type="jpg"): 		try: 			self.MergeFile() 		except: 			pass  		SavePath=filedialog.asksaveasfilename() 		if (SavePath.split('.'))[-1]!=f_type:     			SavePath=(SavePath.split('.'))[0]+'.'+f_type 		if f_type == 'pdf': 			self.DocumentList[0].save(SavePath,save_all=True,append_images=self.DocumentList[1:]) 		else: 			self.DocumentImage.save(SavePath)   	def NextPage(self): 		try: 			self.MergeFile() 			self.DocumentList[self.CurentPage]=self.DocumentImage 		except: 			pass  		if (len(self.DocumentList)-1) > self.CurentPage: 			self.CurentPage+=1 		self.DocumentImage=self.DocumentList[self.CurentPage]  		self.SignImage = None 		self.SignImgLink = None 		self.SignObj = None  		self.DocFile(True)    	def PrevPage(self): 		try: 			self.MergeFile() 			self.DocumentList[self.CurentPage]=self.DocumentImage 		except: 			pass  		if self.CurentPage>0: 			self.CurentPage-=1 		self.DocumentImage=self.DocumentList[self.CurentPage]  		self.SignImage = None 		self.SignImgLink = None 		self.SignObj = None  		self.DocFile(True)     root = Tk() root.title("Documents signer") DocCan = DocCanv(root, width=canvas_size, height=canvas_size) DocCan.pack(side='right', fill=BOTH, expand=1) MenuFrame = Frame(root, width=120, bg='gray22') MenuFrame.pack(side='right', fill=Y)  OpenDocBtn = Button(MenuFrame, text='Open Document',command=DocCan.DocFile) OpenDocBtn.pack(fill=X, padx=5,pady=3) SignDocBtn = Button(MenuFrame, text='Open sign',command=DocCan.SignFile) SignDocBtn.pack(fill=X, padx=5,pady=3) SavePDFBtn = Button(MenuFrame, text='Save as pdf',command = lambda arg1=DocCan, arg2='pdf': DocCanv.SaveFile(arg1,arg2)) SavePDFBtn.pack(fill=X, padx=5,pady=3) SaveJPGBtn = Button(MenuFrame, text='Save as jpg',command = lambda arg1=DocCan, arg2='jpg': DocCanv.SaveFile(arg1,arg2)) SaveJPGBtn.pack(fill=X, padx=5,pady=3)  NextPageBtn = Button(MenuFrame, text='Next page',command = DocCan.NextPage) NextPageBtn.pack(fill=X, padx=5,pady=3) PrevPageBtn = Button(MenuFrame, text='Prev page',command = DocCan.PrevPage) PrevPageBtn.pack(fill=X, padx=5,pady=3)  DocCan.bind("<B1-Motion>", DocCan.MoveSign) DocCan.bind("<MouseWheel>", DocCan.ResizeSign)   root.mainloop() 

ссылка на оригинал статьи https://habr.com/ru/post/549116/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *