취미삼아 배우는 프로그래밍

PDF파일 다루는 프로그램 만들어보기. 본문

파이썬

PDF파일 다루는 프로그램 만들어보기.

Nadure 2020. 7. 5. 16:32

PyQt5는 강의하는 강좌도 되게 많고, 좋은 내용도 상당히 많습니다.

그냥 PyQt기존 강좌처럼 따라가려 하다가,

생각해 보니까 PDF를 다루는 프로그램들이 죄다 유료여서 회사에서 쓰기 불편한게 생각나더라구요

그래서 만들어 보려고 합니다.

회사에서 안 사주니 더러워서 직접 만들어 쓰는 PDF툴

시작합니다.

일단 커다란 틀 정도는 잡고 가야겠죠

메인 기능은

1. PDF 나누기 : 선택한 파일을 그림 파일로 쪼갠다.

2. PDF 합치기 : 선택한 파일 pdf 파일들을 모두 한 파일로 합친다.

3. 페이지 추출 : 선택한 파일(들)의 한 페이지를 일괄 추출한다.

정도로 생각해야겠네요.

필요한 기능이 있으면 추가를 해보구요.

1. 틀 만들기

# 자 오늘 사용해볼 Pdf 라이브러리는 PyPDF2입니다.
# 설치는 pip install PyPDF2를 해주세요.

from PyPDF2 import PdfFileReader, PdfFileWriter


class HandlePdfFile:
    def __init__(self, file_path):
        if type(file_path) != list:
            raise Exception('파일 경로의 입력 형태가 리스트가 아닙니다.')

    @classmethod
    def merge_pdf(cls, file_path_list):
        pass
    
    @classmethod
    def split_pdf(cls, file_path_list):
        pass

    @classmethod
    def extract_pageFromPdf(cls, file_path_list, target_page):
        pass


# 테스트 코드
if __name__ == "__main__":
    HandlePdfFile.merge_pdf(['asdfa'])

비교적 형태는 간단하게 잡았습니다.

그냥 함수형으로 써도 되겠지만, 임포트를 한 번에 하기 위해서 전부 클래스 안에 넣었습니다.

일단 저는 모든 파일들에 대해서 리스트 형식으로 받을 계획입니다.

그래서 부모 클래스에게 파일경로 변수가 리스트로 오는건지를 먼저 체크하는걸 넣었죠.

이렇게 되면

cls(file_path_list)를 호출함과 동시에 변수의 타입등을 한 번에 체크가 가능해져요.

이번엔 파일이 실제 있는지의 여부와

아웃풋 폴더가 존재하는지에 대해 예외체크를 추가해 보도록 할게요.

 

# 자 오늘 사용해볼 Pdf 라이브러리는 PyPDF2입니다.
# 설치는 pip install PyPDF2를 해주세요.

from PyPDF2 import PdfFileReader, PdfFileWriter
import os


class HandlePdfFile:
    def __init__(self, file_path):
        self.output_folder = 'output'
        # 예외 확인
        
        # 타입 체크
        if type(file_path) != list:
            raise Exception('파일 경로의 입력 형태가 리스트가 아닙니다.')
        
        # 파일 경로 체크
        for f in file_path:
            if not os.path.exists(f):
                raise Exception('파일 경로에 파일이 없습니다.')
        
        # 아웃풋 폴더 체크
        if not os.path.exists(self.output_folder):
            os.makedirs(self.output_folder)

    @classmethod
    def merge_pdf(cls, file_path_list, output_path='output.pdf'):
        cls(file_path_list)
    
    @classmethod
    def split_pdf(cls, file_path_list):
        pass

    @classmethod
    def extract_pageFromPdf(cls, file_path_list, target_page):
        pass

    @classmethod
    def create_pdfFromImages(cls, file_path_list):
        pass

# 테스트 코드
if __name__ == "__main__":
    HandlePdfFile.merge_pdf(['asdfa'])

 

 

테스트 화면

 

 

저는 보통 예외체크를 제일 마지막에 넣거나 귀찮으니 패스하긴 하는데,

이번엔 한 번 먼저 만들어 봤습니다.

이리저리 생각을 하다가 classmethod를 처음 구조적으로 사용해봤는데,

제 주관적으로 상당히 재밌는 구조로 틀을 만들었네요.

자 그럼 이제 각 함수에 내용을 채워보도록 하겠습니다.

각 함수는

https://blog.naver.com/goglkms/221817310571

 

제가 쓴 이 글을 참조했습니다.

먼저,

* PDF 페이지 나누기

    @classmethod
    def split_pdf(cls, file_path_list):
        this = cls(file_path_list)
        output_path = os.path.join(os.getcwd(), this.output_folder) 
        print(output_path)
        for file_path in file_path_list:
            basename = os.path.basename(file_path)
            fname = os.path.splitext(basename)[0]
            # 경로로부터 파일 읽기
            pdf = PdfFileReader(file_path)

            for page in range(pdf.getNumPages()):
                pdf_writer = PdfFileWriter()
                pdf_writer.addPage(pdf.getPage(page))

                output_filename = f'{fname}_page_{page + 1}.pdf'

                with open(os.path.join(output_path, output_filename),
                     'wb') as out:
                    pdf_writer.write(out)

 

* PDF 합치기

    @classmethod
    def merge_pdf(cls, file_path_list, output_path='output.pdf'):
        this = cls(file_path_list)
        pdf_writer = PdfFileWriter()
        for f in file_path_list:
            pdf_reader = PdfFileReader(f)
            for page in range(pdf_reader.getNumPages()):
                pdf_writer.addPage(pdf_reader.getPage(page))
        with open(output_path, 'wb') as complete:
            pdf_writer.write(complete)

 

* PDF 페이지 추출

    @classmethod
    def extract_pageFromPdf(cls, file_path_list, target_page):
        this = cls(file_path_list)
        for file_path in file_path_list:            
            basename = os.path.basename(file_path)
            fname = os.path.splitext(basename)[0]
                
            print(fname)
            pdf = PdfFileReader(file_path)

            # 예외 처리 : 페이지가 존재하지 않는 경우
            if pdf.getNumPages() < target_page:
                raise Exception('타겟 페이지가 없습니다.')

            pdf_writer = PdfFileWriter()
            pdf_writer.addPage(pdf.getPage(target_page))
            output_filename = f'extracted_{fname}_page_{target_page + 1}.pdf'

            with open(os.path.join(os.getcwd(), this.output_folder, output_filename),
                'wb') as out:
                pdf_writer.write(out)

 

* 전체 코드

# pdf_handle.py

# 자 오늘 사용해볼 Pdf 라이브러리는 PyPDF2입니다.
# 설치는 pip install PyPDF2를 해주세요.

from PyPDF2 import PdfFileReader, PdfFileWriter
import os


class HandlePdfFile:
    def __init__(self, file_path):
        self.output_folder = 'output'
        # 예외 확인
        
        # 타입 체크
        if type(file_path) != list:
            raise Exception('파일 경로의 입력 형태가 리스트가 아닙니다.')
        
        # 파일 경로 체크
        for f in file_path:
            if not os.path.exists(f):
                raise Exception('파일 경로에 파일이 없습니다.')
        
        # 아웃풋 폴더 체크
        if not os.path.exists(self.output_folder):
            os.makedirs(self.output_folder)

    @classmethod
    def merge_pdf(cls, file_path_list, output_path='output.pdf'):
        this = cls(file_path_list)
        pdf_writer = PdfFileWriter()
        for f in file_path_list:
            pdf_reader = PdfFileReader(f)
            for page in range(pdf_reader.getNumPages()):
                pdf_writer.addPage(pdf_reader.getPage(page))
        with open(output_path, 'wb') as complete:
            pdf_writer.write(complete)

    @classmethod
    def split_pdf(cls, file_path_list):
        this = cls(file_path_list)
        output_path = os.path.join(os.getcwd(), this.output_folder) 
        print(output_path)
        for file_path in file_path_list:
            basename = os.path.basename(file_path)
            fname = os.path.splitext(basename)[0]
            # 경로로부터 파일 읽기
            pdf = PdfFileReader(file_path)

            for page in range(pdf.getNumPages()):
                pdf_writer = PdfFileWriter()
                pdf_writer.addPage(pdf.getPage(page))

                output_filename = f'{fname}_page_{page + 1}.pdf'

                with open(os.path.join(output_path, output_filename),
                     'wb') as out:
                    pdf_writer.write(out)

    @classmethod
    def extract_pageFromPdf(cls, file_path_list, target_page):
        this = cls(file_path_list)
        for file_path in file_path_list:            
            basename = os.path.basename(file_path)
            fname = os.path.splitext(basename)[0]
                
            print(fname)
            pdf = PdfFileReader(file_path)

            # 예외 처리 : 페이지가 존재하지 않는 경우
            if pdf.getNumPages() < target_page:
                raise Exception('타겟 페이지가 없습니다.')

            pdf_writer = PdfFileWriter()
            pdf_writer.addPage(pdf.getPage(target_page))
            output_filename = f'extracted_{fname}_page_{target_page + 1}.pdf'

            with open(os.path.join(os.getcwd(), this.output_folder, output_filename),
                'wb') as out:
                pdf_writer.write(out)



# 테스트 코드
if __name__ == "__main__":
    # 
    HandlePdfFile.split_pdf(['pdf_sample.pdf'])
    HandlePdfFile.merge_pdf(['pdf_sample.pdf', 'pdf_sample2.pdf'])
    HandlePdfFile.extract_pageFromPdf(['pdf_sample.pdf', 'pdf_sample2.pdf'],2)
    # HandlePdfFile.extract_pageFromPdf(
    #     [r'C:/Users/Nadure/Desktop/pyqt_example/pdf_sample.pdf', r'C:/Users/Nadure/Desktop/pyqt_example/pdf_sample2.pdf']
    #     , 1)

 

 

GUI를 만들어 줍시다.

 

 

병합
분할
추출

UI 파일다운로드

그 다음 해야할 거는,

파일을 선택하는 화면을 띄우고, gui와 연결해줘야할 듯 합니다.

한 번, 파일을 선택하는 함수를 버튼들과 연결 시켜 봅시다.

그치만 파일을 선택하는 함수가 총 세 개고, 세 개는 모두 다른 버튼입니다.

파일을 선택하는 코드는 모두 같지만, 저장되는 영역은 각기 다르기 때문에

복붙해서 코드를 작성해야 하는 것 처럼 보입니다.

그치만 유지보수를 위해서라면 손대기 쉬울 만큼 최대한 간결하게 쓰는게 좋다고 생각합니다.

그러니, 중복코드를 줄이려면 무언가 다른 수를 써야 합니다.

여러 방법들이 많겠지만, 저는 따로 action이라는 변수를 전달받고,

이 변수의 값에 따라 분기점을 주는 방식을 택했습니다.

그리고 버튼들을 연결하는 부분은 그냥 모아서 진행하려 합니다.

 

import sys, os
from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog
from PyQt5 import uic

form_class = uic.loadUiType("main.ui")[0]


class MyWindow(QMainWindow, form_class):
    def __init__(self):
        super().__init__()
        self.setupUi(self)

        # 결과물을 저장할 경로 리스트
        self.mergePathList = []
        self.splitPathList = []
        self.extractPathList = []
        
        self.connect_Btn()
        
    def connect_Btn(self):
        '''
        파일을 선택하는 함수를 실행.
        lambda는 변수를 입력하기 위해 추가됐다.
        원래의 형태는 .connect(self.fileSelect) 로 작성해야 정상적이다.
        그치만 변수를 넣어줄 수 없기 때문에 아래와 같은 형식을 취했다.
        그 말인즉, select를 연결 시킬 건데, 이 때의 select는 
        self.fileSelect(변수) 를 넣은 것을 select로 하여 연결시키게 한다.
        '''
        self.mergeSelectBtn.clicked.connect( lambda select: self.fileSelect('merge') )
        self.splitSelectBtn.clicked.connect( lambda select: self.fileSelect('split') )
        self.extractSelectBtn.clicked.connect( lambda select: self.fileSelect('extract') )
    
    def fileSelect(self, action):
        # 들어오는 action에 따라 조금씩 다르게 작동하도록 분기점을 넣었다.
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        fileNames, _ = QFileDialog.getOpenFileNames(self,
            f"Open {action}", 
            "","Python Files (*.pdf);;All Files (*)", options=options)
        if fileNames:
            print( fileNames )
            for f in fileNames :
                name = os.path.split(f)[-1]
                if action == 'merge' and f not in self.mergePathList:
                    self.mergePathList.append(f)
                    self.mergeListWidget.addItem(name)

                elif action == 'split' and f not in self.splitPathList:
                    self.splitPathList.append(f)
                    self.splitListWidget.addItem(name)

                elif action == 'extract' and f not in self.extractPathList:
                    self.extractPathList.append(f)
                    self.extractListWidget.addItem(name)
    

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = MyWindow()
    myWindow.show()
    app.exec_()

 

* 목록 지우는 함수 추가

# ... 중략
# 내용 추가
    def clear_list(self, action):
        if action == 'merge':
            self.mergeListWidget.clear()
            self.mergePathList = []

        if action == 'split':
            self.splitListWidget.clear()
            self.splitPathList = []
        
        if action == 'extract':
            self.extractListWidget.clear()
            self.extractPathList = []

 

* 버튼 등록

    def connect_Btn(self):
        '''
        파일을 선택하는 함수를 실행.
        lambda는 변수를 입력하기 위해 추가됐다.
        원래의 형태는 .connect(self.fileSelect) 로 작성해야 정상적이다.
        그치만 변수를 넣어줄 수 없기 때문에 아래와 같은 형식을 취했다.
        그 말인즉, select를 연결 시킬 건데, 이 때의 select는 
        self.fileSelect(변수) 를 넣은 것을 select로 하여 연결시키게 한다.
        '''
        self.mergeSelectBtn.clicked.connect( lambda select: self.fileSelect('merge') )
        self.splitSelectBtn.clicked.connect( lambda select: self.fileSelect('split') )
        self.extractSelectBtn.clicked.connect( lambda select: self.fileSelect('extract') )

        self.mergeClearBtn.clicked.connect( lambda select: self.clear_list('merge') )
        self.splitClearBtn.clicked.connect( lambda select: self.clear_list('split') )
        self.extractClearBtn.clicked.connect( lambda select: self.clear_list('extract') )

 

마찬가지로, 메인 함수를 실행시키는 부분을 추가시키고

버튼에 연결합니다.

아, 아까 만든 pdf 다루는 걸 임포트하는것도 추가하구요.

 

    def do_startMainFunction(self, action):
        # HandlePdfFile.split_pdf(['pdf_sample.pdf'])
        # HandlePdfFile.merge_pdf(['pdf_sample.pdf', 'pdf_sample2.pdf'])
        # HandlePdfFile.extract_pageFromPdf(['pdf_sample.pdf', 'pdf_sample2.pdf'], 1)
        if action == 'merge':
            HandlePdfFile.merge_pdf(self.mergePathList)
            print('merged')

        if action == 'split':
            HandlePdfFile.split_pdf(self.splitPathList)
            print('splited')

        if action == 'extract':
            target_page = self.extractPageNo.value()
            HandlePdfFile.extract_pageFromPdf(self.extractPathList, target_page-1)
            print('extracted')

 

버튼 추가

    def connect_Btn(self):
        '''
        파일을 선택하는 함수를 실행.
        lambda는 변수를 입력하기 위해 추가됐다.
        원래의 형태는 .connect(self.fileSelect) 로 작성해야 정상적이다.
        그치만 변수를 넣어줄 수 없기 때문에 아래와 같은 형식을 취했다.
        그 말인즉, select를 연결 시킬 건데, 이 때의 select는 
        self.fileSelect(변수) 를 넣은 것을 select로 하여 연결시키게 한다.
        '''
        self.mergeSelectBtn.clicked.connect( lambda select: self.fileSelect('merge') )
        self.splitSelectBtn.clicked.connect( lambda select: self.fileSelect('split') )
        self.extractSelectBtn.clicked.connect( lambda select: self.fileSelect('extract') )

        self.mergeClearBtn.clicked.connect( lambda select: self.clear_list('merge') )
        self.splitClearBtn.clicked.connect( lambda select: self.clear_list('split') )
        self.extractClearBtn.clicked.connect( lambda select: self.clear_list('extract') )

        self.mergeStartBtn.clicked.connect( lambda select: self.do_startMainFunction('merge') )
        self.splitStartBtn.clicked.connect( lambda select: self.do_startMainFunction('split') )
        self.extractStartBtn.clicked.connect( lambda select: self.do_startMainFunction('extract') )

 

 

전체코드입니다.

import sys, os
from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog
from PyQt5 import uic
from pdf_handle import HandlePdfFile

form_class = uic.loadUiType("main.ui")[0]


class MyWindow(QMainWindow, form_class):
    def __init__(self):
        super().__init__()
        self.setupUi(self)

        # 결과물을 저장할 경로 리스트
        self.mergePathList = []
        self.splitPathList = []
        self.extractPathList = []        
        self.connect_Btn()
        
    def connect_Btn(self):
        '''
        파일을 선택하는 함수를 실행.
        lambda는 변수를 입력하기 위해 추가됐다.
        원래의 형태는 .connect(self.fileSelect) 로 작성해야 정상적이다.
        그치만 변수를 넣어줄 수 없기 때문에 아래와 같은 형식을 취했다.
        그 말인즉, select를 연결 시킬 건데, 이 때의 select는 
        self.fileSelect(변수) 를 넣은 것을 select로 하여 연결시키게 한다.
        '''
        self.mergeSelectBtn.clicked.connect( lambda select: self.fileSelect('merge') )
        self.splitSelectBtn.clicked.connect( lambda select: self.fileSelect('split') )
        self.extractSelectBtn.clicked.connect( lambda select: self.fileSelect('extract') )

        self.mergeClearBtn.clicked.connect( lambda select: self.clear_list('merge') )
        self.splitClearBtn.clicked.connect( lambda select: self.clear_list('split') )
        self.extractClearBtn.clicked.connect( lambda select: self.clear_list('extract') )

        self.mergeStartBtn.clicked.connect( lambda select: self.do_startMainFunction('merge') )
        self.splitStartBtn.clicked.connect( lambda select: self.do_startMainFunction('split') )
        self.extractStartBtn.clicked.connect( lambda select: self.do_startMainFunction('extract') )
    
    def fileSelect(self, action):
        # 들어오는 action에 따라 조금씩 다르게 작동하도록 분기점을 넣었다.
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        fileNames, _ = QFileDialog.getOpenFileNames(self,
            f"Open {action}", 
            "","Python Files (*.pdf);;All Files (*)", options=options)
        if fileNames:
            print( fileNames )
            for f in fileNames :
                name = os.path.split(f)[-1]
                if action == 'merge' and f not in self.mergePathList:
                    self.mergePathList.append(f)
                    self.mergeListWidget.addItem(name)

                elif action == 'split' and f not in self.splitPathList:
                    self.splitPathList.append(f)
                    self.splitListWidget.addItem(name)

                elif action == 'extract' and f not in self.extractPathList:
                    self.extractPathList.append(f)
                    self.extractListWidget.addItem(name)
    
    def clear_list(self, action):
        if action == 'merge':
            self.mergeListWidget.clear()
            self.mergePathList = []

        if action == 'split':
            self.splitListWidget.clear()
            self.splitPathList = []
        
        if action == 'extract':
            self.extractListWidget.clear()
            self.extractPathList = []

    def do_startMainFunction(self, action):
        # HandlePdfFile.split_pdf(['pdf_sample.pdf'])
        # HandlePdfFile.merge_pdf(['pdf_sample.pdf', 'pdf_sample2.pdf'])
        # HandlePdfFile.extract_pageFromPdf(['pdf_sample.pdf', 'pdf_sample2.pdf'], 1)
        if action == 'merge':
            HandlePdfFile.merge_pdf(self.mergePathList)
            print('merged to output.pdf')

        if action == 'split':
            HandlePdfFile.split_pdf(self.splitPathList)
            print('splited')

        if action == 'extract':
            target_page = self.extractPageNo.value()
            HandlePdfFile.extract_pageFromPdf(self.extractPathList, target_page-1)
            print('extracted')

if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWindow = MyWindow()
    myWindow.show()
    app.exec_()

 

작동모습

 

소스 코드 파일

빌드된 exe파일

 

* blog.naver.com/goglkms

이거 제 블로그임미다 !

 

Comments