파이썬 예제 (웹페이지 PDF 변환)

들어가며

이번 주제는 파이썬과 Qt의 QtWebEngine을 이용해 웹페이지를 앱에 띄우고 해당페이지를 PDF 파일 저장, 인쇄하는 프로그램을 만드는 것입니다.

예전에 작성한 게시물 중 PDF 변환 예제가 있는데, 이미지로 캡쳐해 변환하는 방식이라 확대시 글자가 흐릿해지는 현상을 방지하는 보완 예제로 만들어 보았습니다.

결과물을 먼저 살펴보면 아래와 같습니다.

(웹 브라우저가 아니라 Python 앱에 QWebEngineView 클래스 배치후 웹페이지를 띄운 모습)

[앱 실행화면]

앱을 실행 후 PDF, PRINT 버튼을 누르면 파일저장, 인쇄창이 뜨는데 캡쳐된 동영상에서는 보이지 않네요.

아래 링크를 통해 직접 한번 사용해 보기 바랍니다. 

(파이썬 가상환경으로 개발하지 않아 실행파일 크기가 큽니다. 약 100 MB)

Pyinstaller 제작 실행파일(*.exe) 링크 :  WebViewer


개발환경

  • Windows 10 Pro, Pycharm 2020.3.1
  • Python 3.8.6 (64bit)
  • PyQt5 5.15.2
  • PyQtWebEngine (PyQt5 외 별도 설치 필요)

PyQt5 예전 버전에는 PyQt 설치시 QtWebEngine이 같이 설치되었던거 같은데, 지금 (5.15.2기준)은 따로 설치해야 합니다. 

필요시 아래 패키지를 따로 설치 바랍니다.

python -m pip install PyQtWebEngine

 

소스코드

자세한 설명은 전체 코드를 살펴보고 진행하겠습니다.

from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog, QDialog
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import Qt, QUrl, QEvent, QEventLoop
from PyQt5.QtPrintSupport import QPrinter, QPrintDialog
from PyQt5.QtGui import QPainter
import sys

QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)

class Form(QWidget):

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

        self.home = QUrl('https://google.com')

        hbox = QHBoxLayout()
        self.btn_go = QPushButton('Move')
        self.btn_pdf = QPushButton('PDF')
        self.btn_prn = QPushButton('Print')
        self.le_url = QLineEdit(self.home.toString())
        hbox.addWidget(self.le_url)
        hbox.addWidget(self.btn_go)
        hbox.addWidget(self.btn_pdf)
        hbox.addWidget(self.btn_prn)
        hbox.setStretchFactor(self.le_url, 10)

        self.webview = QWebEngineView()
        self.webview.load(self.home)

        vbox = QVBoxLayout(self)
        vbox.addLayout(hbox)
        vbox.addWidget(self.webview)

        self.setWindowTitle('Ocean Coding School')
        self.setLayout(vbox)

        # signal
        QApplication.instance().installEventFilter(self)
        self.webview.urlChanged.connect(self.onUrlChanged)
        self.btn_go.clicked.connect(self.onMove)
        self.btn_pdf.clicked.connect(self.onPDF)
        self.btn_prn.clicked.connect(self.onPrint)

    def eventFilter(self, obj, e):
        if obj == self.le_url and e.type() == QEvent.KeyPress and e.key() == Qt.Key_Return:
            self.onMove()
            return True
        return super().eventFilter(obj, e)

    def onUrlChanged(self, url):
        self.le_url.setText(url.toString())
        self.le_url.home(True)

    def onMove(self):
        txt = self.le_url.text()
        if txt.find('http')==-1 and txt.find('https')==-1:
            txt = f'http://{txt}'
            self.le_url.setText(txt)

        url = QUrl(txt)
        self.webview.load(url)

    def onPDF(self):
        page = self.webview.page()
        path = QFileDialog.getSaveFileName(self, 'Save PDF', '', 'PDF File(*.pdf)')
        if path[0]:
            page.printToPdf(path[0])

    def onPrint(self):
        printer = QPrinter()
        printer.setResolution(QPrinter.HighResolution)
        printer.setPageSize(QPrinter.A4)
        dlg = QPrintDialog(printer, self.webview)
        if dlg.exec_() != QDialog.Accepted:
            return

        loop = QEventLoop()
        result = False

        def printPreView(success):
            nonlocal result
            result = success
            loop.quit()

        page = self.webview.page()
        page.print(printer, printPreView)
        loop.exec_()

        if not result:
            qp = QPainter()
            if qp.begin(printer):
                font = qp.font()
                font.setPixelSize(20)
                qp.setFont(font)
                qp.drawText(10, 25, "Could not generate print preview")
                qp.end()

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


[1~8 라인]

필요한 파이썬 Site-Packages 를 불러오는 부분, 8번 라인은 4K 모니터의 해상도를 조정해 주는 코드.


[10~43 라인]

전체 코드는 하나의 메인함수와 Form이라는 하나의 클래스로 구성, 해당 코드는 QWidget에서 상속받은 Form 클래스의 생성자 함수.

Qt Designer를 사용하지 않은 예제이므로, 대부분의 코드는 UI컨트롤을 생성하는 내용.

먼저 수평박스(hbox)에 아래 컨트롤 4개 추가.

[앱 상부 컨트롤]

이후, QWebEngineView 위젯을 생성, 수직박스(vbox)에 위에서 만든 hbox를 배치 후 웹뷰 클래스를 추가.

[완성된 UI 모습]

이어서 QWebEngineView의 load() 함수를 이용 구글페이지 표시.

생성자의 마지막 작업으로 시그널 연결 작업.

1. 전역 엔터키 이벤트 처리 [39번 라인]

2. 웹 페이지 변경시 이벤트 처리 [40번 라인]

3. 페이지 이동, PDF, 인쇄 버튼 이벤트 처리 [41,42,43 라인]


[45~49 라인]

URL 입력컨트롤에 포커스가 있고, 엔터키가 입력된 경우 바로 해당 페이지로 이동처리.

QLineEdit 클래스를 오버라이딩해 키보드 이벤트를 처리하는 방법에 비해 간편.


[51~53 라인]

QWebEngineView의 페이지가 변경되면 호출되는 슬롯함수.

페이지 변경을 감지해 QLineEdit에 해당 URL 표시


[55~62 라인]

이동 버튼 클릭시 호출되는 슬롯함수.

URL을 읽어와 "http" or "https" 가 없다면 추가하고 해당 페이지로 이동.


[64~68 라인]

PDF 저장 버튼 클릭시 호출되는 슬롯함수.

QWebEngineView에서 해당 페이지 (QWebEnginePage)를 가져와 페이지의 printToPdf() 함수를 이용해 PDF변환 저장.

예) 네이버 이동후 PDF 변환 (캡쳐 변환이 아님, 벡터 방식으로 확대후 깨짐X)

[PDF 변환 파일]

[70~88 라인]

인쇄 버튼 클릭시 호출되는 슬롯함수.

1. QPrinter class 객체를 생성하고 프린터 설정. (고해상도 모드, A4사이즈)

2. 프린트 인쇄창 생성 후 인쇄 선택시 인쇄 시작. (PDF 출력선택시 파일로도 저장 가능)

[Print Dialog]

3. QEventLoop 객체를 생성하고 QWebEnginePage 클래스의 print() 함수로 인쇄

4. 해당 print() 함수는 두번째 전달인자로 인쇄과정의 성공여부를 판단하는 CallBack Function을 전달해야 하며, printPreView() 중첩함수 (Nested Function)가 그 역할을 수행.

세부적인 내용은 아래 인용된 Qt Document를 참조.

(저는 PyQt 사용시 C++ Qt의 문서를 참조해 코드를 작성합니다.)

[QWebEnginePage, print() 설명 인용]

 

[실제 프린트로 인쇄 결과]

 

[99~103 라인]

메인함수, QApplication 및 Form Class 객체 생성 후 실행.

여기까지가 코드 분석입니다.

 

참고 자료

Link : Qt WebEngine Example (C++)

Qt WebEngine은 오픈소스 웹프로젝트인 Chromium(크로미엄) 기반으로 작성.

구글 크롬 브라우저도 크로미엄 기반입니다.

[Qt 인용 : WebEngine Architecture]

 

예제에서는 View, Page만 다루어 보았고 히스토리 기능 자바 스크립트도 사용가능합니다.

[Qt 인용 : WebWidget 모듈]

보다 자세한 내용은 Qt WebEngine 도움말을 참조 바랍니다.


이상으로 모든 설명을 마칩니다.

감사합니다.

댓글

  1. 안녕하세요.

    혹시 dialog 없이 바로 기본 프린터로 인쇄를 할 수 있을까요?
    식당에서 영수증 출력 누르면 전용 프린트로 바로 인쇄 되는 것 처럼요...
    감사합니다.

    답글삭제
    답글
    1. QPrintDialog 와 관계된 코드를 주석처리하면 됩니다.

      삭제
  2. 와아~~~ 답변 주실지 몰랐는데... 이렇게 답변해 주셔서 너무너무 고맙습니다 ^^

    아래 세 라인을 주석처리하고 해 보았습니다.(어제)

    dlg = QPrintDialog(printer, self.webview)
    if dlg.exec_() != QDialog.Accepted:
    return

    그런데... 프린터로 전송을 하기는 하나
    글씨가 인쇄되어야 하는데 그 영역만큼 구름처럼 뭉퉁하게 나옵니다.

    그래서 아래와 같이도 해 보았습니다.
    dlg = QPrintDialog(printer, self.webview)
    dlg.accept()
    # if dlg.exec_() != QDialog.Accepted:
    # return

    마찬가지네요 ㅡ.ㅡ
    뭔가 전송을하기는 하는데... 제대로 전송이 프린터로 되지 않는가 봅니다.

    혹시 해결방법이 있을까요?

    아래는 프린터로 인쇄된 종이를 캡처해서 올려놓은 사진입니다.
    https://pay.kkjk.co.kr/test/images/none_qdialog.jpg

    답글삭제
    답글
    1. 그렇군요.

      아마도 QPrintDialog 생성자 함수로 전달되는 printer 객체에 출력과 관계된 설정이 이루어지는것으로 보입니다.

      시간이 없어 테스트 해보진 않았지만, 프린터 설정창을 띄우고 싶지 않다면, 디버그 모드로 실행해 아래의 변화를 직접 처리하면 됩니다.

      1. QPrintDialog가 실행되기전 BreakPoint를 설정하고 printer 객체의 속성(Attribute)을 저장해 둡니다.

      2. QPrintDialog가 실행후 printer 객체 속성의 변화를 위와 비교해 차이점을 알아둡니다.

      3. QPrintDialog를 주석처리하고 printer객체의 속성을 수동으로 설정하면 됩니다.

      삭제

댓글 쓰기

이 블로그의 인기 게시물

Qt Designer 설치하기

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