파이썬 예제 (PDF 파일저장, 인쇄)

이번에 만든 예제는 python과 PyQt5를 이용해 pdf 파일 생성 프린터 인쇄 에 대한 주제를 다룹니다.

pyinstaller로 제작된 실행파일 링크: 다운로드

프로그램의 실행 모습은 아래와 같습니다.

화면의 내용이 출력된 결과를 보여주기 위해 더미 테이블 데이터를 생성하고 버튼 2개로 구성해 보았습니다.

[프로그램 실행화면]

하나의 더미 QTableWidget 과 2개의 QPushButton으로 구성되어 있습니다.

1. 'Print PDF File' 버튼을 누른 경우 (프린터 창 띄우고 인쇄)


[인쇄 창 열기]

[프린터를 통해 실제 인쇄된 모습]

자세한 내용은 이어지는 코드 분석에서 설명드리겠지만, 대략 아래의 흐름으로 진행됩니다.
  • QPrinter class 생성 (프린터)
  • QPrinterDialog class 생성 (프린터 설정창)
  • QPainter class 생성 (화면 출력 클래스)
  • 출력하고자 하는 위젯(QTableWidget)을 QPainter에 맞춤
  • QTableWidget의 render() 함수를 이용한 인쇄

인쇄창에서 실제 프린터가 아닌 'Microsoft Print To PDF'를 선택한 경우, PDF파일 생성도 가능합니다.


2. 'Make PDF File' 버튼을 누른 경우 (PDF 파일 생성)


프로젝트의 기본 경로 (변경 가능)에 다음과 같은 PDF 파일 생성

[PDF 파일 생성]

test.pdf 파일을 열어보면 아래와 같이 전체 위젯이 캡쳐된 모습을 확인할 수 있습니다.

[PDF 파일 내용]
아래의 순서로 진행된 결과입니다.
  • QPdfWriter class 생성
  • 화면 캡쳐 후, QPixmap class 이미지 생성
  • 이미지 크기 Resizing
  • QPainter class 생성
  • QPainter.drawPixmap() 함수로 pdf 파일 생성

[소스 코드]

from PyQt5.QtWidgets import QApplication, QWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QPushButton, QDialog
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPdfWriter, QPagedPaintDevice, QPainter, QScreen, QPixmap
from PyQt5.QtPrintSupport import QPrinter, QPrintDialog
import sys
import random

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

class MyWidget(QWidget):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        vbox = QVBoxLayout()

        # 테이블 위젯생성(더미 데이터 용)
        self.table = QTableWidget(self)
        self.table.setRowCount(10)
        self.table.setColumnCount(3)
        col = ['first', 'second', 'third']
        self.table.setHorizontalHeaderLabels(col)
        for r in range(self.table.rowCount()):
            for c in range(self.table.columnCount()):
                self.table.setItem(r, c, QTableWidgetItem(str(random.randint(1,100))))

        # 버튼 생성
        self.btn = QPushButton('Print PDF File', self)
        self.btn.clicked.connect(self.btnClick)

        self.btn2 = QPushButton('Make PDF file', self)
        self.btn2.clicked.connect(self.btnClick2)

        # 컨트롤들 박스 레이아웃 배치
        vbox.addWidget(self.table)
        vbox.addWidget(self.btn)
        vbox.addWidget(self.btn2)
        self.setLayout(vbox)
        self.resize(600,400)

    def btnClick(self):
        # 프린터 생성, 실행
        printer = QPrinter()
        dlg = QPrintDialog(printer, self)
        if dlg.exec() == QDialog.Accepted:
            # Painter 생성
            qp = QPainter()
            qp.begin(printer)        

            # 여백 비율
            wgap = printer.pageRect().width()*0.1
            hgap = printer.pageRect().height()*0.1

            # 화면 중앙에 위젯 배치
            xscale = (printer.pageRect().width()-wgap)/self.table.width()
            yscale = (printer.pageRect().height()-hgap)/self.table.height()
            scale = xscale if xscale < yscale else yscale        
            qp.translate(printer.paperRect().x() + printer.pageRect().width()/2, printer.paperRect().y() + printer.pageRect().height()/2)
            qp.scale(scale, scale);
            qp.translate(-self.table.width()/2, -self.table.height()/2);        

            # 인쇄
            self.table.render(qp)

            qp.end()       

    def btnClick2(self):
        # pdf 생성
        pdf = QPdfWriter('test.pdf')
        pdf.setPageSize(QPagedPaintDevice.A4)

        # 화면 캡쳐        
        screen = QApplication.primaryScreen()
        img = screen.grabWindow(self.winId(), 0,0, self.rect().width(),self.rect().height())

        # 3항 연산자 (a if test else b, 만약 test가 참이면 a, 아니면 b)
        # 이미지 크기는 큰 값 기준, PDF 크기는 작은값 기준(화면 초과 방지)
        img_size = img.width() if img.width()-img.height() > 0 else img.height()
        pdf_size = pdf.width() if pdf.width()-pdf.height() < 0 else pdf.height()

        # 최적 비율 얻기
        ratio = pdf_size / img_size
        
        # pdf에 쓰기
        qp = QPainter()
        qp.begin(pdf)
        qp.drawPixmap(0, 0, img.width()*ratio, img.height()*ratio, img)
        qp.end()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MyWidget()
    w.show()
    sys.exit(app.exec_())

코드가 살짝 복잡해 보이지만 더미 테이블을 생성하고 출력물을 예쁘게 배치하기 위한 리사이징 코드를 제외하고, PDF 생성과 인쇄 코드는 10줄 내외로 간단합니다.

1~6번 라인은 이 예제를 사용하기 위해 필요한 모듈을 불러오는 코드입니다.

10번 라인부터 시작되는 MyWidget class에서 모든 작업이 이루어 입니다.

12번 라인 생성자 함수는 MyWidget class가 QWidget 에서 상속받으므로 부모 클래스의 생성자 함수를 호출하고, UI 배치를 위한 initUI() 함수를 호출합니다.

16번 라인 initUI() 함수는 컨트롤 배치를 위해 수직 레이아웃박스(vbox) 를 생성하고, QTableWidget의 객체를 생성해 더미 데이터 아래와 같이 생성합니다.

[QTableWidget 더미 데이터]

이어서 QPushButton class를 이용해 버튼을 2개 생성하고 앞서 만들어둔 수직박스(vbox)에 위젯을 추가하면 아래와 같은 모습이 됩니다.

[테이블위젯,버튼 배치 모습]

물론 버튼을 눌렀을때 발생되는 시그널 처리도 작성(31, 34번 라인)해 두어야 합니다.

이제 위쪽 버튼을 누르면 btnClick() 함수가 아래쪽 버튼은 btnClick2() 함수가 호출되게 됩니다.

여기까지 화면을 생성하고 컨트롤 배치, 시그널 처리를 위한 준비 작업이 모두 완료되었습니다.

43번 라인 btnClick() 함수는 'Print PDF File' 버튼을 누르면 호출되는 함수이며, 바로 여기서 인쇄 작업이 처리됩니다.

QPrinter 클래스를 생성하고, 인쇄 창을 위한 QPrintDialog 클래스를 생성합니다.

이어서 QPrintDialog 의 exec() 함수를 호출해 모달 대화상자로 인쇄창을 띄웁니다.

이때 인쇄창에서 설정을 마치고 인쇄 버튼을 누르면 모달상태로 열린 인쇄설정창 exec() 함수가 QDialog.Accepted 를 리턴하고, 이 경우 인쇄 작업이 일어나도록 조건문으로 처리합니다. 
[인쇄 버튼을 누른 경우]

바로 이 코드 입니다.
if dlg.exec() == QDialog.Accepted:

이제 출력을 위한 QPainter class를 생성합니다. 이름이 비슷하지만 QPrinter가 아닙니다.

그리고 begin(프린터) 함수를 통해 QPainter가 출력할 대상(Paint Device)이 모니터 화면이 아니라 프린터라고 인식시킵니다.

바로 출력을 진행해도 되지만 화면에 꽉 차서 보기 싫으므로, 여백을 설정하기 위한 코드를 아래와 같이 넣어두었습니다.

프린터 페이지 크기의  약 10%를 가로, 세로 여백으로 잡았습니다.
wgap = printer.pageRect().width()*0.1
hgap = printer.pageRect().height()*0.1

이어서 56~62번 라인은 화면의 중앙에 출력하기 위해 실제 위젯 크기(픽셀)를 프린터 출력 크기로 비율을 조정해 주고 배치하는 코드입니다.

일반적으로 프린터의 해상도는 화면 해상도보다 크기 때문에 이 작업이 없다면 너무 작게 출력되게 됩니다.

아래는 Qt의 Document에서 발췌한 변환(TransFormation) 메카니즘 입니다.

[Qt Transformation]

마지막으로 65번 라인 QTableWidget의 메소드(멤버함수) render() 함수를 호출하고 전달인자로 QPainter class를 넘겨주면 QPainter와 연결된 프린터에 화면이 출력되게 됩니다.


이젠 69번 라인 btnClick2() 함수에서 처리하는 PDF 파일 생성에 대해 살펴보겠습니다.

Qt는 정말 방대한 C++ class의 모음이며, PDF 파일 생성을 위한 QPdfWriter class 를 지원합니다.

먼저 QPdfWriter class 객체를 생성하고, 출력 크기를 A4 로 설정합니다.
pdf = QPdfWriter('test.pdf')
pdf.setPageSize(QPagedPaintDevice.A4)
다음으로 PDF 파일에 화면을 캡쳐해서 넣기위해 화면을 캡쳐합니다.
screen = QApplication.primaryScreen()
img = screen.grabWindow(self.winId(), 0,0, self.rect().width(),self.rect().height()) 
이제 캡쳐된 이미지를 PDF에 쓰면 되지만, 보기 좋게 출력하고 PDF 화면 크기에 맞도록 조정하는 작업을 거칩니다.
img_size = img.width() if img.width()-img.height() > 0 else img.height()
pdf_size = pdf.width() if pdf.width()-pdf.height() < 0 else pdf.height()

ratio = pdf_size / img_size
파이썬의 3항 연산자 구문이며, 3항 연산자에 대한 내용은 생략하겠습니다. ratio 변수는 PDF의 크기와 캡쳐한 화면의 픽셀을 한 화면에 담기위해 비율을 조정한 값이 저장됩니다.

이제 PDF 파일 생성과 캡쳐된 화면 이미지의 비율 조정을 마쳤으므로, PDF파일에 쓰기만 하면 파일이 생성됩니다.
qp = QPainter()
qp.begin(pdf)
qp.drawPixmap(0, 0, img.width()*ratio, img.height()*ratio, img)
qp.end()


PDF 파일에 출력하기 위해 QPainter Class를 생성하고 begin() 함수의 전달인자로 대상 장치(PaintDevice)가 pdf 파일임을 알려줍니다.

다음으로 drawPixmap() 함수를 이용해 출력영역 (0, 0, 조정된 너비, 조정된 높이) 에 출력하면 PDF파일에 이미지가 출력되게 됩니다.


마지막으로 인쇄를 처리하거나 PDF파일에 내용을 출력하는 위 코드가 잘 이해되지 않는다면 Qt의 QPainter를 이용한 Drawing 메커니즘에 대한 이해가 아직 부족한 것입니다.

간략히 설명하자면 QPainter는 그림을 그리는 '팔, Hand'이며, QPrinter, QPDFWriter는 그림이 그려질 '도화지, Target' 이라고 생각하면 좀 이해가 쉽습니다.

그림을 그리는 팔로 인쇄 영역에 그림을 그리면 출력이 일어나고, PDF파일에 그리면 PDF파일에 쓰여지는 원리입니다.

감사합니다.

웹페이지 인쇄와 PDF 저장에 대한 문의가 많아 해당 주제에 대한 보충예제를 작성하였으니 참조바랍니다.

링크 : 웹페이지 PDF저장 및 인쇄

 

댓글

  1. 안녕하세요 저는 웹페이지 전체(스크롤 모두 내린 page)를 print to PDF 로 변환하고 싶은데
    img = screen.grabWindow(self.winId(), 0,0, self.rect().width(),self.rect().height())

    이 라인을 어찌 수정 해야할지 감이 안와서 댓글달게 되었습니다
    댓글주시면 참고해서 수정해보겠습니다!!

    답글삭제
    답글
    1. 웹페이지가 Qt 위젯 or 메인윈도우에 불려진 형태(Qt WebEngine)인가요? 아니면 별도의 인터넷 브라우저 페이지를 pdf 변환하는 경우인가요?

      삭제
    2. 답글 감사합니다 PyQt5에 불려진 형태입니다~

      삭제
    3. 이미지로 캡쳐해 저장한다면 아래와 같이 진행하면 됩니다.
      다만, QPixmap 생성시 위젯의 size를 웹페이지의 전체 크기로 잡아야 합니다.

      # 화면 캡쳐 (기존)
      screen = QApplication.primaryScreen()
      img = screen.grabWindow(self.winId(), 0, 0, self.rect().width(), self.rect().height())
      img.save('test1.png')

      # 위젯의 render() 활용
      img = QPixmap(self.table.size());
      self.table.render(img)
      img.save('test2.png')

      삭제
    4. 감사합니다! 지금 스파이더가 실행이 안되서 그러는데 조치되면 알려주신방법대로 해보겠습니다^^

      삭제
  2. 브라우저에 띄워진 전체 페이지를 print to PDF로 저장하고 싶으면 어떻게 해야하나요?

    답글삭제
    답글
    1. 쉬운 방법은 브라우저의 URL 주소를 얻은후 내 프로그램에서 페이지를 띄운 후(QtWebEngine) PDF로 변환하는 것입니다.

      만약 내 프로그램에서 별도의 윈도우(브라우저)의 페이지를 가져오는 경우라면, OS의 API 함수를 이용해 실행중인 윈도우들의 프로세스 ID를 알아낸 후 브라우저 윈도우를 선택해 화면을 캡쳐하는 방식으로 접근해야 합니다.

      Windows 운영체제라면 블로그에 "화면캡쳐"로 검색하면 C++로 작성된 캠쳐 프로그램을 참조하기 바랍니다.

      삭제
  3. 조언을 얻고자 합니다. qt 디자이너로 gui 작성해서 코딩중입니다. 버튼 누르면 mainwindow 화면에 나와있는 상태를 pdf 파일로 저장 하고자 합니다. 도움 바랍니다.

    답글삭제
    답글
    1. 예제코드의 74번 라인에 화면캡쳐와 PDF저장이 이미 설명되어 있습니다.

      삭제
  4. 버튼을 누르면 엑셀파일을 pdf파일로 변환하여 저장하고 싶은데 위의 코드와 비슷할까요?

    답글삭제
    답글
    1. 엑셀을 열어 데이터를 불러들이고 PDF로 저장하는 일반적인 과정입니다.

      1. openpyxl or pandas 등 이용 excel file open.
      2. 예제의 QPdfWriter or Python pdf lib 등 활용해 pdf write.

      https://www.geeksforgeeks.org/creating-pdf-documents-with-python/

      삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

C++ 예제 (소켓 서버, 이미지, 파일전송)