Index: doc/changelog.md
==================================================================
--- doc/changelog.md
+++ doc/changelog.md
@@ -1,9 +1,13 @@
# 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
Index: mtg_proxy_printer/print.py
==================================================================
--- mtg_proxy_printer/print.py
+++ mtg_proxy_printer/print.py
@@ -13,20 +13,30 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
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
@@ -37,10 +47,12 @@
"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.")
@@ -50,10 +62,38 @@
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)
ADDED mtg_proxy_printer/resources/icons/breeze/actions/16/document-export.svg
Index: mtg_proxy_printer/resources/icons/breeze/actions/16/document-export.svg
==================================================================
--- /dev/null
+++ mtg_proxy_printer/resources/icons/breeze/actions/16/document-export.svg
@@ -0,0 +1,11 @@
+
+
ADDED mtg_proxy_printer/resources/icons/breeze/actions/22/document-export.svg
Index: mtg_proxy_printer/resources/icons/breeze/actions/22/document-export.svg
==================================================================
--- /dev/null
+++ mtg_proxy_printer/resources/icons/breeze/actions/22/document-export.svg
@@ -0,0 +1,11 @@
+
+
ADDED mtg_proxy_printer/resources/icons/breeze/actions/24/document-export.svg
Index: mtg_proxy_printer/resources/icons/breeze/actions/24/document-export.svg
==================================================================
--- /dev/null
+++ mtg_proxy_printer/resources/icons/breeze/actions/24/document-export.svg
@@ -0,0 +1,13 @@
+
+
ADDED mtg_proxy_printer/resources/icons/breeze/actions/32/document-export.svg
Index: mtg_proxy_printer/resources/icons/breeze/actions/32/document-export.svg
==================================================================
--- /dev/null
+++ mtg_proxy_printer/resources/icons/breeze/actions/32/document-export.svg
@@ -0,0 +1,11 @@
+
+
Index: mtg_proxy_printer/resources/resources.qrc
==================================================================
--- mtg_proxy_printer/resources/resources.qrc
+++ mtg_proxy_printer/resources/resources.qrc
@@ -33,10 +33,11 @@
icons/breeze/actions/16/application-exit.svg
icons/breeze/actions/16/configure.svg
icons/breeze/actions/16/dialog-cancel.svg
icons/breeze/actions/16/dialog-ok.svg
icons/breeze/actions/16/document-close.svg
+ icons/breeze/actions/16/document-export.svg
icons/breeze/actions/16/document-import.svg
icons/breeze/actions/16/document-new.svg
icons/breeze/actions/16/document-open.svg
icons/breeze/actions/16/document-print-direct.svg
icons/breeze/actions/16/document-print-preview.svg
@@ -66,10 +67,11 @@
icons/breeze/actions/22/application-exit.svg
icons/breeze/actions/22/configure.svg
icons/breeze/actions/22/dialog-cancel.svg
icons/breeze/actions/22/dialog-ok.svg
icons/breeze/actions/22/document-close.svg
+ icons/breeze/actions/22/document-export.svg
icons/breeze/actions/22/document-import.svg
icons/breeze/actions/22/document-new.svg
icons/breeze/actions/22/document-open.svg
icons/breeze/actions/22/document-print-direct.svg
icons/breeze/actions/22/document-print-preview.svg
@@ -99,10 +101,11 @@
icons/breeze/actions/24/application-exit.svg
icons/breeze/actions/24/configure.svg
icons/breeze/actions/24/dialog-cancel.svg
icons/breeze/actions/24/dialog-ok.svg
icons/breeze/actions/24/document-close.svg
+ icons/breeze/actions/24/document-export.svg
icons/breeze/actions/24/document-import.svg
icons/breeze/actions/24/document-new.svg
icons/breeze/actions/24/document-open.svg
icons/breeze/actions/24/document-print-direct.svg
icons/breeze/actions/24/document-print-preview.svg
@@ -132,10 +135,11 @@
icons/breeze/actions/32/application-exit.svg
icons/breeze/actions/32/configure.svg
icons/breeze/actions/32/dialog-cancel.svg
icons/breeze/actions/32/dialog-ok.svg
icons/breeze/actions/32/document-close.svg
+ icons/breeze/actions/32/document-export.svg
icons/breeze/actions/32/document-import.svg
icons/breeze/actions/32/document-new.svg
icons/breeze/actions/32/document-open.svg
icons/breeze/actions/32/document-print-direct.svg
icons/breeze/actions/32/document-print.svg
Index: mtg_proxy_printer/resources/ui/main_window.ui
==================================================================
--- mtg_proxy_printer/resources/ui/main_window.ui
+++ mtg_proxy_printer/resources/ui/main_window.ui
@@ -35,10 +35,11 @@
+
@@ -364,10 +365,21 @@
Add custom cards
+
+
+
+
+
+
+ Create image sequence
+
+
+ Export document as an image sequence
+
CentralWidget
Index: mtg_proxy_printer/ui/dialogs.py
==================================================================
--- mtg_proxy_printer/ui/dialogs.py
+++ mtg_proxy_printer/ui/dialogs.py
@@ -49,10 +49,11 @@
logger = get_logger(__name__)
del get_logger
__all__ = [
"SavePDFDialog",
+ "SavePNGDialog",
"SaveDocumentAsDialog",
"LoadDocumentDialog",
"AboutDialog",
"PrintPreviewDialog",
"PrintDialog",
@@ -110,10 +111,52 @@
@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__()
Index: mtg_proxy_printer/ui/main_window.py
==================================================================
--- mtg_proxy_printer/ui/main_window.py
+++ mtg_proxy_printer/ui/main_window.py
@@ -34,11 +34,11 @@
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
@@ -191,11 +191,14 @@
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)
@@ -302,10 +305,22 @@
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)