1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
| # Copyright © 2020-2025 Thomas Hess <thomas.hess@udo.edu>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
from pathlib import Path
from PyQt5.QtCore import QObject, QMarginsF, QSizeF, pyqtSlot as Slot, QPersistentModelIndex
from PyQt5.QtGui import QPainter, QPdfWriter, QPageSize
from PyQt5.QtPrintSupport import QPrinter
import mtg_proxy_printer.meta_data
from mtg_proxy_printer.settings import settings
from mtg_proxy_printer.model.document import Document
from mtg_proxy_printer.model.page_layout import PageLayoutSettings
from mtg_proxy_printer.ui.page_scene import RenderMode, PageScene
from mtg_proxy_printer.logger import get_logger
import mtg_proxy_printer.units_and_sizes
logger = get_logger(__name__)
del get_logger
__all__ = [
"export_pdf",
"create_printer",
"Renderer",
]
def export_pdf(document: Document, file_path: str, parent: QObject = None):
pages_to_print = settings["pdf-export"].getint("pdf-page-count-limit") or document.rowCount()
if not pages_to_print: # No pages in document. Return now, to avoid dividing by zero
logger.error("Tried to export a document with zero pages as a PDF. Aborting.")
return
logger.info(f'Exporting document with {document.rowCount()} pages as PDF to "{file_path}"')
total_documents = math.ceil(document.rowCount()/pages_to_print)
for document_index in range(total_documents):
logger.info(f"Creating PDF ({document_index+1}/{total_documents}) with up to {pages_to_print} pages.")
printer = PDFPrinter(document, file_path, parent, document_index, pages_to_print)
printer.print_document()
def create_printer(renderer: "Renderer") -> QPrinter:
printer = QPrinter(QPrinter.PrinterMode.HighResolution)
layout = renderer.document.page_layout
page_layout = layout.to_page_layout(renderer.render_mode)
if not printer.setPageLayout(page_layout):
logger.error(
f"Setting page layout failed! "
f"Layout: page_size={page_layout.pageSize().size(QPageSize.Unit.Millimeter)}, "
f"orientation={page_layout.orientation()}, "
f"margins={layout.margin_left, layout.margin_top, layout.margin_right, layout.margin_bottom}")
# magnitude returns a float by default, so round to int to avoid a TypeError
printer.setResolution(round(mtg_proxy_printer.units_and_sizes.RESOLUTION.magnitude))
# Disable duplex printing by default
printer.setDoubleSidedPrinting(False)
printer.setDuplex(QPrinter.DuplexMode.DuplexNone)
printer.setOutputFormat(QPrinter.OutputFormat.NativeFormat)
if RenderMode.IMPLICIT_MARGINS not in renderer.render_mode:
printer.setFullPage(True)
return printer
class PDFPrinter(QPdfWriter):
"""
Exports the given document to PDF.
Can be given an optional index and length parameter to only export a chunk of the document for splitting purposes.
"""
def __init__(self, document: Document, file_path: str, parent: QObject = None,
document_index: int = 0, pages_to_print: int = None):
"""
Constructs a new PDFPrinter.
:param document: Document to export
:param file_path: file path for the PDF output. If pages_to_print is set and less than the total page count,
the output file will be numbered, by appending a dash-separated numerical suffix to the file name stem.
:param parent: Qt object parent
:param document_index: Document sequence number. Used to compute the range of pages to be exported
:param pages_to_print: Number of pages to export. Default value None means "all pages"
"""
self.document = document
self.document_index = document_index
self.pages_to_print = pages_to_print = pages_to_print or document.rowCount()
self.landscape_workaround_enabled = settings["pdf-export"].getboolean("landscape-compatibility-workaround")
if pages_to_print < document.rowCount():
# Determine the number of digits required to properly sort all documents, without having to rely on
# external support for natural sorting
suffix_length = len(str(math.ceil(document.rowCount() / pages_to_print)))
# Add one to the document_index for human-readable counting starting at 1
suffix = str(document_index+1).zfill(suffix_length)
path = Path(file_path)
file_path = str(path.with_stem(f"{path.stem}-{suffix}"))
super().__init__(file_path)
self.setParent(parent)
self.setCreator(f"{mtg_proxy_printer.meta_data.PROGRAMNAME}, v{mtg_proxy_printer.meta_data.__version__}")
self.painter = QPainter()
# magnitude returns a float by default, so round to int to avoid a TypeError
self.setResolution(round(mtg_proxy_printer.units_and_sizes.RESOLUTION.magnitude))
self.setPageSize(self._to_page_size(document.page_layout))
# Prevent downscaling the page content
self.setPageMargins(QMarginsF(0, 0, 0, 0))
self.scene = PageScene(document, RenderMode.ON_PAPER, self)
logger.info(f"Created {self.__class__.__name__} instance.")
def _to_page_size(self, layout: PageLayoutSettings) -> QPageSize:
"""Converts PageLayoutSettings to QPageSize"""
size = QSizeF(layout.page_width.magnitude, layout.page_height.magnitude)
if layout.page_width > layout.page_height and self.landscape_workaround_enabled:
size.transpose()
return QPageSize(size, QPageSize.Unit.Millimeter)
def print_document(self):
logger.info("Begin rendering PDF document.")
layout = self.document.page_layout
scaling = 1
self.painter.begin(self)
if layout.page_width > layout.page_height and self.landscape_workaround_enabled:
scaling = self.scene.width()/self.scene.height()
self.painter.rotate(90)
self.painter.translate(0, -self.scene.height())
self.painter.setRenderHint(QPainter.LosslessImageRendering) # Prevent avoidable image degradation
self.painter.scale(
scaling*self.logicalDpiX()/self.resolution(),
scaling*self.logicalDpiY()/self.resolution(),
)
first_index = self.document_index * self.pages_to_print
last_index = min((self.document_index + 1) * self.pages_to_print, self.document.rowCount())
for page_number in range(first_index, last_index):
logger.debug(f"Rendering page {page_number+1}/{self.document.rowCount()}")
self._switch_to_page(page_number)
self.scene.render(self.painter)
if page_number + 1 < last_index: # Avoid including a trailing, empty page
self.newPage()
self.painter.end()
logger.info("Writing document finished.")
def _switch_to_page(self, page_number: int):
"""Render the given page on the internal scene"""
index = QPersistentModelIndex(self.document.index(page_number, 0))
self.scene.on_current_page_changed(index)
class Renderer(QObject):
def __init__(self, document: Document, parent: QObject = None):
super().__init__(parent)
self.document = document
self.render_mode = RenderMode.ON_PAPER
if not settings["printer"].getboolean("borderless-printing"):
self.render_mode |= RenderMode.IMPLICIT_MARGINS
self.scene = PageScene(document, self.render_mode, self)
@Slot(QPrinter)
def print_document(self, printer: QPrinter):
logger.info("Begin printing document.")
landscape_workaround_enabled = settings["printer"].getboolean("landscape-compatibility-workaround")
is_landscape_document = self.scene.width() > self.scene.height()
painter = QPainter(printer)
if is_landscape_document and landscape_workaround_enabled:
painter.rotate(90)
painter.translate(0, -self.scene.height())
scaling = self.scene.width()/self.scene.height()
painter.scale(scaling, scaling)
painter.setRenderHint(QPainter.LosslessImageRendering)
page_count = self.document.rowCount()
for index in range(page_count):
logger.debug(f"Printing page {index+1}/{page_count}")
self.scene.on_current_page_changed(QPersistentModelIndex(self.document.index(index, 0)))
self.scene.render(painter)
if index+1 < page_count:
printer.newPage()
painter.end()
logger.info("Printing document finished.")
|