MTGProxyPrinter

print.py at tip
Login

print.py at tip

File mtg_proxy_printer/print.py from the latest check-in


     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.")