MTGProxyPrinter

Changes On Branch png_export
Login

Changes On Branch png_export

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch png_export Excluding Merge-Ins

This is equivalent to a diff from 63a062bb69 to 7e276d3b1f

2025-05-08
11:41
Create new branch named "static_cardb_fixture" Leaf check-in: cd3715f19f user: thomas tags: static_cardb_fixture
2025-05-07
15:39
Merge with trunk Leaf check-in: 1caa0e5983 user: thomas tags: port_pyside6
15:35
Sync with trunk. Leaf check-in: 7e276d3b1f user: thomas tags: png_export
15:34
Cleaned up development environment creation scripts and the build environment requirements. tox and pip-tools aren't required in the environment used to build the wheel file. Leaf check-in: 63a062bb69 user: thomas tags: trunk
15:03
Add changelog entry check-in: ebb0164021 user: thomas tags: png_export
10:23
Add multiple user experience improvements check-in: 836b4bdf29 user: thomas tags: trunk

Changes to doc/changelog.md.

1
2
3
4




5
6
7
8
9
10
11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15




+
+
+
+







# Changelog

# Next version (in development)

## New features

- Export documents as a lossless PNG image sequence. The export can be triggered via the File menu.

## Changed features

- The main window now opens maximized when starting the application. Added a setting to restore the previous behavior.
- Added option to open wizards and dialogs maximized. Off by default.
- Reduced click count required for switching printings from 5 to 3. Now, double-clicking editable table cells 
  automatically opens the list with choices. Clicking an entry in the list saves immediately.

Changes to mtg_proxy_printer/print.py.

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
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







+
+
+
+
+
+
+
+

+

-
-
+
+






+














+
+













+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







#  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 functools import partial

from mtg_proxy_printer.units_and_sizes import RESOLUTION

try:
    from os import process_cpu_count
except ImportError:
    from os import cpu_count as process_cpu_count
from pathlib import Path
from threading import BoundedSemaphore

from PyQt5.QtCore import QObject, QMarginsF, QSizeF, pyqtSlot as Slot, QPersistentModelIndex
from PyQt5.QtGui import QPainter, QPdfWriter, QPageSize
from PyQt5.QtCore import QObject, QMarginsF, QSizeF, pyqtSlot as Slot, QPersistentModelIndex, QThreadPool
from PyQt5.QtGui import QPainter, QPdfWriter, QPageSize, QImage
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.model.carddb import with_database_write_lock
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

RenderHint = QPainter.RenderHint

__all__ = [
    "export_pdf",
    "create_printer",
    "Renderer",
]

PNGEncoderThreadLimit = BoundedSemaphore(max(1, process_cpu_count()-1))


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 export_png(document: Document, file_path: str, parent: QObject = None):
    file_path = Path(file_path)
    page_count = document.rowCount()
    if not page_count:  # No pages in document
        logger.error("Tried to export a document with zero pages. Aborting.")
        return
    logger.info(f'Exporting document with {document.rowCount()} pages as PNG image sequence to "{file_path}"')
    page_size = document.page_layout.to_page_layout(RenderMode.ON_PAPER).pageSize().sizePixels(
        round(RESOLUTION.magnitude))
    pool = QThreadPool.globalInstance()
    scene = PageScene(document, RenderMode.ON_PAPER, parent)
    number_width = len(str(page_count))
    parent = file_path.parent
    for page_nr in range(page_count):
        file_name = f"{file_path.stem}-{str(page_nr+1).zfill(number_width)}.png"
        output_path = parent / file_name
        image = QImage(page_size, QImage.Format.Format_RGB888)
        painter = QPainter(image)
        scene.on_current_page_changed(document.index(page_nr, 0))
        scene.render(painter)
        pool.start(partial(_compress_single_image, image, output_path))


@with_database_write_lock(PNGEncoderThreadLimit)
def _compress_single_image(image: QImage, output_path: Path):
    image.save(str(output_path), "PNG", 0)


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(

Added mtg_proxy_printer/resources/icons/breeze/actions/16/document-export.svg.












1
2
3
4
5
6
7
8
9
10
11
+
+
+
+
+
+
+
+
+
+
+
<!DOCTYPE svg>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <style type="text/css" id="current-color-scheme">
            .ColorScheme-Text {
                color:#232629;
            }
        </style>
    </defs>
    <path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 7 12 L 11.0859 12 L 9.46484 13.6211 L 10.1719 14.3281 L 13 11.5 L 10.1719 8.67187 L 9.46484 9.37891 L 11.0859 11 L 7 11 L 7 12 Z M 4 13 L 4 3 L 9 3 L 9 6 L 12 6 L 12 9 L 13 9 L 13 5 L 10 2 L 3 2 L 3 14 L 8 14 L 8 13 L 4 13 Z"/>
</svg>

Added mtg_proxy_printer/resources/icons/breeze/actions/22/document-export.svg.












1
2
3
4
5
6
7
8
9
10
11
+
+
+
+
+
+
+
+
+
+
+
<!DOCTYPE svg>
<svg viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <style type="text/css" id="current-color-scheme">
            .ColorScheme-Text {
                color:#232629;
            }
        </style>
    </defs>
    <path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 11 16 L 16.293 16 L 14 18.293 L 14.707 19 L 18.207 15.5 L 14.707 12 L 14 12.707 L 16.293 15 L 11 15 L 11 16 Z M 5 18 L 5 4 L 13 4 L 13 8 L 17 8 L 17 13 L 18 13 L 18 7 L 14 3 L 4 3 L 4 19 L 13 19 L 13 18 L 5 18 Z"/>
</svg>

Added mtg_proxy_printer/resources/icons/breeze/actions/24/document-export.svg.














1
2
3
4
5
6
7
8
9
10
11
12
13
+
+
+
+
+
+
+
+
+
+
+
+
+
<!DOCTYPE svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" version="1.1" width="24" height="24">
  <defs>
    <style type="text/css" id="current-color-scheme">
            .ColorScheme-Text {
                color:#232629;
            }
        </style>
  </defs>
  <g transform="translate(1,1)">
    <path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 11 16 L 16.293 16 L 14 18.293 L 14.707 19 L 18.207 15.5 L 14.707 12 L 14 12.707 L 16.293 15 L 11 15 L 11 16 Z M 5 18 L 5 4 L 13 4 L 13 8 L 17 8 L 17 13 L 18 13 L 18 7 L 14 3 L 4 3 L 4 19 L 13 19 L 13 18 L 5 18 Z"/>
  </g>
</svg>

Added mtg_proxy_printer/resources/icons/breeze/actions/32/document-export.svg.












1
2
3
4
5
6
7
8
9
10
11
+
+
+
+
+
+
+
+
+
+
+
<!DOCTYPE svg>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
    <defs>
        <style type="text/css" id="current-color-scheme">
            .ColorScheme-Text {
                color:#232629;
            }
        </style>
    </defs>
    <path class="ColorScheme-Text" style="fill:currentColor; fill-opacity:1; stroke:none" d="M 7 5 L 18 5 L 18 12 L 25 12 L 25 19 L 26 19 L 26 11 L 19 4 L 6 4 L 6 28 L 19 28 L 19 27 L 7 27 L 7 5 Z M 16.9961 22.5 L 16.9961 23.5 L 25.0859 23.5 L 21.293 27.293 L 22 28 L 27 23 L 22 18 L 21.293 18.707 L 25.0859 22.5 L 16.9961 22.5 Z"/>
</svg>

Changes to mtg_proxy_printer/resources/resources.qrc.

31
32
33
34
35
36
37

38
39
40
41
42
43
44
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45







+







    <!-- actions -->
    <!-- 16x16 -->
    <file>icons/breeze/actions/16/application-exit.svg</file>
    <file>icons/breeze/actions/16/configure.svg</file>
    <file>icons/breeze/actions/16/dialog-cancel.svg</file>
    <file>icons/breeze/actions/16/dialog-ok.svg</file>
    <file>icons/breeze/actions/16/document-close.svg</file>
    <file>icons/breeze/actions/16/document-export.svg</file>
    <file>icons/breeze/actions/16/document-import.svg</file>
    <file>icons/breeze/actions/16/document-new.svg</file>
    <file>icons/breeze/actions/16/document-open.svg</file>
    <file>icons/breeze/actions/16/document-print-direct.svg</file>
    <file>icons/breeze/actions/16/document-print-preview.svg</file>
    <file>icons/breeze/actions/16/document-print.svg</file>
    <file>icons/breeze/actions/16/document-properties.svg</file>
64
65
66
67
68
69
70

71
72
73
74
75
76
77
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79







+







    
    <!-- 22x22 -->
    <file>icons/breeze/actions/22/application-exit.svg</file>
    <file>icons/breeze/actions/22/configure.svg</file>
    <file>icons/breeze/actions/22/dialog-cancel.svg</file>
    <file>icons/breeze/actions/22/dialog-ok.svg</file>
    <file>icons/breeze/actions/22/document-close.svg</file>
    <file>icons/breeze/actions/22/document-export.svg</file>
    <file>icons/breeze/actions/22/document-import.svg</file>
    <file>icons/breeze/actions/22/document-new.svg</file>
    <file>icons/breeze/actions/22/document-open.svg</file>
    <file>icons/breeze/actions/22/document-print-direct.svg</file>
    <file>icons/breeze/actions/22/document-print-preview.svg</file>
    <file>icons/breeze/actions/22/document-print.svg</file>
    <file>icons/breeze/actions/22/document-properties.svg</file>
97
98
99
100
101
102
103

104
105
106
107
108
109
110
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113







+







    
    <!-- 24x24 -->
    <file>icons/breeze/actions/24/application-exit.svg</file>
    <file>icons/breeze/actions/24/configure.svg</file>
    <file>icons/breeze/actions/24/dialog-cancel.svg</file>
    <file>icons/breeze/actions/24/dialog-ok.svg</file>
    <file>icons/breeze/actions/24/document-close.svg</file>
    <file>icons/breeze/actions/24/document-export.svg</file>
    <file>icons/breeze/actions/24/document-import.svg</file>
    <file>icons/breeze/actions/24/document-new.svg</file>
    <file>icons/breeze/actions/24/document-open.svg</file>
    <file>icons/breeze/actions/24/document-print-direct.svg</file>
    <file>icons/breeze/actions/24/document-print-preview.svg</file>
    <file>icons/breeze/actions/24/document-print.svg</file>
    <file>icons/breeze/actions/24/document-properties.svg</file>
130
131
132
133
134
135
136

137
138
139
140
141
142
143
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147







+







    
    <!-- 32x32, not all icons are available at this resolution! -->
    <file>icons/breeze/actions/32/application-exit.svg</file>
    <file>icons/breeze/actions/32/configure.svg</file>
    <file>icons/breeze/actions/32/dialog-cancel.svg</file>
    <file>icons/breeze/actions/32/dialog-ok.svg</file>
    <file>icons/breeze/actions/32/document-close.svg</file>
    <file>icons/breeze/actions/32/document-export.svg</file>
    <file>icons/breeze/actions/32/document-import.svg</file>
    <file>icons/breeze/actions/32/document-new.svg</file>
    <file>icons/breeze/actions/32/document-open.svg</file>
    <file>icons/breeze/actions/32/document-print-direct.svg</file>
    <file>icons/breeze/actions/32/document-print.svg</file>
    <file>icons/breeze/actions/32/document-properties.svg</file>
    <file>icons/breeze/actions/32/document-replace.svg</file>

Changes to mtg_proxy_printer/resources/ui/main_window.ui.

33
34
35
36
37
38
39

40
41
42
43
44
45
46
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47







+







    <addaction name="action_new_document"/>
    <addaction name="action_load_document"/>
    <addaction name="action_save_document"/>
    <addaction name="action_save_as"/>
    <addaction name="action_print_preview"/>
    <addaction name="action_print"/>
    <addaction name="action_print_pdf"/>
    <addaction name="action_export_png"/>
    <addaction name="separator"/>
    <addaction name="action_import_deck_list"/>
    <addaction name="action_add_custom_cards"/>
    <addaction name="separator"/>
    <addaction name="action_quit"/>
   </widget>
   <widget class="QMenu" name="menu_settings">
362
363
364
365
366
367
368











369
370
371
372
373
374
375
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387







+
+
+
+
+
+
+
+
+
+
+







  <action name="action_add_custom_cards">
   <property name="icon">
    <iconset theme="list-add"/>
   </property>
   <property name="text">
    <string>Add custom cards</string>
   </property>
  </action>
  <action name="action_export_png">
   <property name="icon">
    <iconset theme="document-export"/>
   </property>
   <property name="text">
    <string>Create image sequence</string>
   </property>
   <property name="toolTip">
    <string>Export document as an image sequence</string>
   </property>
  </action>
 </widget>
 <customwidgets>
  <customwidget>
   <class>CentralWidget</class>
   <extends>QWidget</extends>
   <header>mtg_proxy_printer.ui.central_widget</header>

Changes to mtg_proxy_printer/ui/dialogs.py.

47
48
49
50
51
52
53

54
55
56
57
58
59
60
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61







+








EventType = QEvent.Type
logger = get_logger(__name__)
del get_logger

__all__ = [
    "SavePDFDialog",
    "SavePNGDialog",
    "SaveDocumentAsDialog",
    "LoadDocumentDialog",
    "AboutDialog",
    "PrintPreviewDialog",
    "PrintDialog",
    "DocumentSettingsDialog",
]
108
109
110
111
112
113
114










































115
116
117
118
119
120
121
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







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







        QThreadPool.globalInstance().start(PrintCountUpdater(self.document))
        logger.info(f"Saved document to {path}")

    @Slot()
    def on_reject(self):
        logger.debug("User aborted saving to PDF. Doing nothing.")


class SavePNGDialog(QFileDialog):

    def __init__(self, parent: QWidget, document: mtg_proxy_printer.model.document.Document):
        # Note: Cannot supply already translated strings to __init__,
        # because tr() requires to have returned from super().__init__()
        super().__init__(parent, "", self.get_preferred_file_name(document))
        self.setWindowTitle(self.tr("Export as PNG"))
        self.setNameFilter(self.tr("PNG images (*.png)"))

        if default_path := read_path("pdf-export", "pdf-export-path"):
            self.setDirectory(default_path)
        self.document = document
        self.setAcceptMode(QFileDialog.AcceptMode.AcceptSave)
        self.setDefaultSuffix("png")
        self.setFileMode(QFileDialog.FileMode.AnyFile)
        self.accepted.connect(self.on_accept)
        self.rejected.connect(self.on_reject)
        logger.info(f"Created {self.__class__.__name__} instance.")

    @staticmethod
    def get_preferred_file_name(document: mtg_proxy_printer.model.document.Document):
        if document.save_file_path is None:
            return ""
        # Note: Qt automatically appends the preferred file extension (.png), if the file does not have one.
        # So ensure it ends on ".png", if there is a dot in the name. Otherwise, let the user enter the name without
        # pre-setting an extension for a cleaner dialog
        stem = document.save_file_path.stem
        return f"{stem}.png" if "." in stem else stem

    @Slot()
    def on_accept(self):
        logger.debug("User chose a file name, about to generate the PNG image sequence")
        path = self.selectedFiles()[0]
        mtg_proxy_printer.print.export_png(self.document, path, self)
        QThreadPool.globalInstance().start(PrintCountUpdater(self.document))
        logger.info(f"Saved document to {path}")

    @Slot()
    def on_reject(self):
        logger.debug("User aborted exporting to PNG. Doing nothing.")


class LoadSaveDialog(QFileDialog):
    def __init__(self, *args, **kwargs):
        # Note: Cannot supply already translated strings to __init__,
        # because tr() requires to have returned from super().__init__()
        super().__init__(*args, **kwargs)
        filter_text = self.tr(

Changes to mtg_proxy_printer/ui/main_window.py.

32
33
34
35
36
37
38
39

40
41
42
43
44
45
46
32
33
34
35
36
37
38

39
40
41
42
43
44
45
46







-
+







from mtg_proxy_printer.document_controller.new_document import ActionNewDocument
from mtg_proxy_printer.document_controller.card_actions import ActionAddCard
from mtg_proxy_printer.ui.custom_card_import_dialog import CustomCardImportDialog
from mtg_proxy_printer.units_and_sizes import DEFAULT_SAVE_SUFFIX
import mtg_proxy_printer.settings
import mtg_proxy_printer.print
from mtg_proxy_printer.ui.dialogs import SavePDFDialog, SaveDocumentAsDialog, LoadDocumentDialog, \
    AboutDialog, PrintPreviewDialog, PrintDialog, DocumentSettingsDialog
    AboutDialog, PrintPreviewDialog, PrintDialog, DocumentSettingsDialog, SavePNGDialog
from mtg_proxy_printer.ui.common import show_wizard_or_dialog
from mtg_proxy_printer.ui.cache_cleanup_wizard import CacheCleanupWizard
from mtg_proxy_printer.ui.deck_import_wizard import DeckImportWizard
from mtg_proxy_printer.ui.progress_bar import ProgressBar

try:
    from mtg_proxy_printer.ui.generated.main_window import Ui_MainWindow
189
190
191
192
193
194
195

196


197
198
199
200
201
202
203
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206







+

+
+







            ui.action_add_empty_card,
            ui.action_discard_page,
            ui.central_widget,
            ui.action_cleanup_local_image_cache,
            ui.action_print,
            ui.action_print_pdf,
            ui.action_print_preview,
            ui.action_export_png,
            ui.action_show_settings,
            ui.action_add_custom_cards,
            ui.action_download_missing_card_images,
        ]

    def _connect_image_database_signals(self, image_db: ImageDatabase):
        image_db.card_download_starting.connect(self.progress_bars.begin_inner_progress)
        image_db.card_download_finished.connect(self.progress_bars.end_inner_progress)
        image_db.card_download_progress.connect(self.progress_bars.set_inner_progress)
        image_db.batch_process_starting.connect(self.progress_bars.begin_outer_progress)
300
301
302
303
304
305
306












307
308
309
310
311
312
313
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328







+
+
+
+
+
+
+
+
+
+
+
+







            "This is passed as the {action} when asking the user about compacting the document if that can save pages")
        if self._ask_user_about_compacting_document(action_str) == StandardButton.Cancel:
            return
        self.current_dialog = dialog = SavePDFDialog(self, self.document)
        dialog.finished.connect(self.on_dialog_finished)
        self.missing_images_manager.obtain_missing_images(dialog.open)

    @Slot()
    def on_action_export_png_triggered(self):
        logger.info(f"User exports the current document to a sequence of PNG images.")
        action_str = self.tr(
            "exporting as a PNG image sequence",
            "This is passed as the {action} when asking the user about compacting the document if that can save pages")
        if self._ask_user_about_compacting_document(action_str) == StandardButton.Cancel:
            return
        self.current_dialog = dialog = SavePNGDialog(self, self.document)
        dialog.finished.connect(self.on_dialog_finished)
        self.missing_images_manager.obtain_missing_images(dialog.open)

    @Slot()
    def on_action_add_empty_card_triggered(self):
        empty_card = self.document.get_empty_card_for_current_page()
        action = ActionAddCard(empty_card)
        self.document.apply(action)

    def on_network_error_occurred(self, message: str):