Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | Implemented drawing 90° sharp card corners. Also fixed 1 pixel card image overlap. |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
87ccc737d116d5b84a5eaf5345c8e586 |
User & Date: | thomas 2022-07-25 14:39:31 |
Context
2022-07-26
| ||
17:40 | Fixed TypeError exception when accepting to open the application download website in the update available notification message. check-in: dcd9adc3c4 user: thomas tags: trunk | |
2022-07-25
| ||
14:39 | Implemented drawing 90° sharp card corners. Also fixed 1 pixel card image overlap. check-in: 87ccc737d1 user: thomas tags: trunk | |
14:37 | Added changelog entry. Closed-Leaf check-in: 60e7187be4 user: thomas tags: draw_sharp_corners | |
2022-07-12
| ||
11:08 | Removed unnecessary @profile decorator in the CardDatabase class. check-in: 6d37b16c2b user: thomas tags: trunk | |
Changes
Changes to doc/changelog.md.
1 2 3 4 5 6 7 8 9 | # Changelog # Version 0.18.0 (2022-07-09) <a name="v0_18_0"></a> ## New features - Proper, full support for oversized cards, like Archenemy schemes or Planechase plane cards. Regular cards and larger cards are always kept on separate pages to ensure that drawn cut marker lines (if enabled) are always 100% accurate. - Note: Some cards, like the Legacy Championship winner rewards, are tagged as being oversized, but are then served | > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # Changelog # Next version (in development) ## New features - Implemented optional drawing of 90° card corners. This can be enabled for all new documents globally in the application settings or individually in the document settings. ## Fixed issues - Fixed card images overlapping by one pixel when image spacing is set to zero. # Version 0.18.0 (2022-07-09) <a name="v0_18_0"></a> ## New features - Proper, full support for oversized cards, like Archenemy schemes or Planechase plane cards. Regular cards and larger cards are always kept on separate pages to ensure that drawn cut marker lines (if enabled) are always 100% accurate. - Note: Some cards, like the Legacy Championship winner rewards, are tagged as being oversized, but are then served |
︙ | ︙ |
Changes to mtg_proxy_printer/model/carddb.py.
︙ | ︙ | |||
13 14 15 16 17 18 19 20 21 22 23 24 25 | # 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 atexit import configparser import dataclasses import datetime import itertools import functools import pathlib import textwrap import typing | > | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | # 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 atexit import configparser import dataclasses import datetime import enum import itertools import functools import pathlib import textwrap import typing from PyQt5.QtGui import QPixmap, QColor from PyQt5.QtCore import Qt, QPoint, QRect, QSize import delegateto import mtg_proxy_printer.app_dirs from mtg_proxy_printer.model.carddb_migrations import migrate_card_database from mtg_proxy_printer.natsort import natural_sorted import mtg_proxy_printer.sqlite_helpers import mtg_proxy_printer.meta_data |
︙ | ︙ | |||
65 66 67 68 69 70 71 72 73 74 75 76 77 78 | # once per month or so. MINIMUM_REFRESH_DELAY = datetime.timedelta(days=14) __all__ = [ "CardIdentificationData", "MTGSet", "Card", "CardDatabase", "cached_dedent", ] @dataclasses.dataclass class CardIdentificationData: | > | 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | # once per month or so. MINIMUM_REFRESH_DELAY = datetime.timedelta(days=14) __all__ = [ "CardIdentificationData", "MTGSet", "Card", "CardCorner", "CardDatabase", "cached_dedent", ] @dataclasses.dataclass class CardIdentificationData: |
︙ | ︙ | |||
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 | elif role == Qt.DisplayRole: return f"{self.name} ({self.code.upper()})" elif role == Qt.ToolTipRole: return self.name else: return None @dataclasses.dataclass(unsafe_hash=True) class Card: name: str = dataclasses.field(compare=True) set: MTGSet = dataclasses.field(compare=True) collector_number: str = dataclasses.field(compare=True) language: str = dataclasses.field(compare=True) scryfall_id: str = dataclasses.field(compare=True) is_front: bool = dataclasses.field(compare=True) oracle_id: str = dataclasses.field(compare=True) image_uri: str = dataclasses.field(compare=False) highres_image: bool = dataclasses.field(compare=False) is_oversized: bool = dataclasses.field(compare=False) face_number: int = dataclasses.field(compare=False) image_file: typing.Optional[QPixmap] = dataclasses.field(default=None, compare=False) def requested_page_type(self) -> PageType: if self.image_file is None: return PageType.OVERSIZED if self.is_oversized else PageType.REGULAR size = self.image_file.size() if (size.width(), size.height()) == (1040, 1490): return PageType.OVERSIZED return PageType.REGULAR OptionalCard = typing.Optional[Card] CardList = typing.List[Card] @functools.lru_cache(None) | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | elif role == Qt.DisplayRole: return f"{self.name} ({self.code.upper()})" elif role == Qt.ToolTipRole: return self.name else: return None @enum.unique class CardCorner(enum.Enum): """ The four corners of a card. Values are relative image positions in X and Y. These are fractions so that they work properly for both regular and oversized cards Values are tuned to return the top-left corner of a 10x10 area centered around (20,20) away from the respective corner. """ TOP_LEFT = (15/745, 15/1040) TOP_RIGHT = (1-25/745, 15/1040) BOTTOM_LEFT = (15/745, 1-25/1040) BOTTOM_RIGHT = (1-25/745, 1-25/1040) @dataclasses.dataclass(unsafe_hash=True) class Card: name: str = dataclasses.field(compare=True) set: MTGSet = dataclasses.field(compare=True) collector_number: str = dataclasses.field(compare=True) language: str = dataclasses.field(compare=True) scryfall_id: str = dataclasses.field(compare=True) is_front: bool = dataclasses.field(compare=True) oracle_id: str = dataclasses.field(compare=True) image_uri: str = dataclasses.field(compare=False) highres_image: bool = dataclasses.field(compare=False) is_oversized: bool = dataclasses.field(compare=False) face_number: int = dataclasses.field(compare=False) image_file: typing.Optional[QPixmap] = dataclasses.field(default=None, compare=False) def set_image_file(self, image: QPixmap): self.image_file = image self.corner_color.cache_clear() def requested_page_type(self) -> PageType: if self.image_file is None: return PageType.OVERSIZED if self.is_oversized else PageType.REGULAR size = self.image_file.size() if (size.width(), size.height()) == (1040, 1490): return PageType.OVERSIZED return PageType.REGULAR @functools.lru_cache(maxsize=len(CardCorner)) def corner_color(self, corner: CardCorner) -> QColor: """Returns the color of the card at the given corner. """ if self.image_file is None: return QColor.fromRgb(255, 255, 255, 0) # fully transparent white sample_area = self.image_file.copy(QRect( QPoint( round(self.image_file.width() * corner.value[0]), round(self.image_file.height() * corner.value[1])), QSize(10, 10) )) average_color = sample_area.scaled(1, 1, transformMode=Qt.SmoothTransformation).toImage().pixelColor(0, 0) return average_color OptionalCard = typing.Optional[Card] CardList = typing.List[Card] @functools.lru_cache(None) |
︙ | ︙ |
Added mtg_proxy_printer/model/document-v5.sql.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 | -- Copyright (C) 2020-2022 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/>. PRAGMA user_version = 0000005; PRAGMA application_id = 41325044; -- ASCII-encoded "MTGP" PRAGMA page_size = 512; VACUUM; -- Required to apply setting PRAGMA page_size CREATE TABLE Card ( page INTEGER NOT NULL CHECK (page > 0), slot INTEGER NOT NULL CHECK (slot > 0), is_front INTEGER NOT NULL CHECK (is_front IN (TRUE, FALSE)) DEFAULT 1, scryfall_id TEXT NOT NULL, PRIMARY KEY(page, slot) ) WITHOUT ROWID; CREATE TABLE DocumentSettings ( rowid INTEGER NOT NULL PRIMARY KEY CHECK (rowid == 1), page_height INTEGER NOT NULL CHECK (page_height > 0), page_width INTEGER NOT NULL CHECK (page_width > 0), margin_top INTEGER NOT NULL CHECK (margin_top >= 0), margin_bottom INTEGER NOT NULL CHECK (margin_bottom >= 0), margin_left INTEGER NOT NULL CHECK (margin_left >= 0), margin_right INTEGER NOT NULL CHECK (margin_right >= 0), image_spacing_horizontal INTEGER NOT NULL CHECK (image_spacing_horizontal >= 0), image_spacing_vertical INTEGER NOT NULL CHECK (image_spacing_vertical >= 0), draw_cut_markers INTEGER NOT NULL CHECK (draw_cut_markers in (TRUE, FALSE)), draw_sharp_corners INTEGER NOT NULL CHECK (draw_sharp_corners in (TRUE, FALSE)) ); |
Changes to mtg_proxy_printer/model/document.py.
︙ | ︙ | |||
452 453 454 455 456 457 458 | ) flattened_data: DocumentSaveFormat = ( (page, slot, scryfall_id, is_front) for (page, (slot, (scryfall_id, is_front))) in itertools.chain.from_iterable(cards) ) with mtg_proxy_printer.sqlite_helpers.open_database( | | | | | 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 | ) flattened_data: DocumentSaveFormat = ( (page, slot, scryfall_id, is_front) for (page, (slot, (scryfall_id, is_front))) in itertools.chain.from_iterable(cards) ) with mtg_proxy_printer.sqlite_helpers.open_database( self.save_file_path, "document-v5", self.loader.MIN_SUPPORTED_SQLITE_VERSION) as db: db.execute("BEGIN TRANSACTION") _migrate_database(db) db.execute("DELETE FROM Card") db.executemany( "INSERT INTO Card (page, slot, scryfall_id, is_front) VALUES (?, ?, ?, ?)", flattened_data ) logger.debug(f"Written {db.execute('SELECT count() FROM Card').fetchone()[0]} cards.") db.execute( textwrap.dedent("""\ INSERT OR REPLACE INTO DocumentSettings (rowid, page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, image_spacing_horizontal, image_spacing_vertical, draw_cut_markers, draw_sharp_corners) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """), dataclasses.astuple(self.page_layout)) logger.debug("Written document settings") db.commit() db.execute("VACUUM") logger.debug("Database saved and closed.") |
︙ | ︙ | |||
743 744 745 746 747 748 749 | self.page_index_cache.clear() self.page_index_cache.update( (id(page), index) for index, page in enumerate(self.pages) ) def _migrate_database(db): | | | > > > > > > > > > > > > > > > > > > > > > > | 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 | self.page_index_cache.clear() self.page_index_cache.update( (id(page), index) for index, page in enumerate(self.pages) ) def _migrate_database(db): if db.execute("PRAGMA user_version\n").fetchone()[0] == 2: for statement in [ "ALTER TABLE Card RENAME TO Card_old", textwrap.dedent("""\ CREATE TABLE Card ( page INTEGER NOT NULL CHECK (page > 0), slot INTEGER NOT NULL CHECK (slot > 0), is_front INTEGER NOT NULL CHECK (is_front IN (0, 1)) DEFAULT 1, scryfall_id TEXT NOT NULL, PRIMARY KEY(page, slot) ) WITHOUT ROWID"""), textwrap.dedent("""\ INSERT INTO Card (page, slot, scryfall_id, is_front) SELECT page, slot, scryfall_id, 1 AS is_front FROM Card_old"""), "DROP TABLE Card_old", "PRAGMA user_version = 3", ]: db.execute(f"{statement};\n") if db.execute("PRAGMA user_version\n").fetchone()[0] == 3: db.execute(textwrap.dedent("""\ CREATE TABLE DocumentSettings ( rowid INTEGER NOT NULL PRIMARY KEY CHECK (rowid == 1), page_height INTEGER NOT NULL CHECK (page_height > 0), page_width INTEGER NOT NULL CHECK (page_width > 0), margin_top INTEGER NOT NULL CHECK (margin_top >= 0), margin_bottom INTEGER NOT NULL CHECK (margin_bottom >= 0), margin_left INTEGER NOT NULL CHECK (margin_left >= 0), margin_right INTEGER NOT NULL CHECK (margin_right >= 0), image_spacing_horizontal INTEGER NOT NULL CHECK (image_spacing_horizontal >= 0), image_spacing_vertical INTEGER NOT NULL CHECK (image_spacing_vertical >= 0), draw_cut_markers INTEGER NOT NULL CHECK (draw_cut_markers in (0, 1)) );""")) db.execute(f"PRAGMA user_version = 4") if db.execute("PRAGMA user_version").fetchone()[0] == 4: for statement in [ "ALTER TABLE DocumentSettings RENAME TO DocumentSettings_Old", textwrap.dedent("""\ CREATE TABLE DocumentSettings ( rowid INTEGER NOT NULL PRIMARY KEY CHECK (rowid == 1), page_height INTEGER NOT NULL CHECK (page_height > 0), page_width INTEGER NOT NULL CHECK (page_width > 0), margin_top INTEGER NOT NULL CHECK (margin_top >= 0), margin_bottom INTEGER NOT NULL CHECK (margin_bottom >= 0), margin_left INTEGER NOT NULL CHECK (margin_left >= 0), margin_right INTEGER NOT NULL CHECK (margin_right >= 0), image_spacing_horizontal INTEGER NOT NULL CHECK (image_spacing_horizontal >= 0), image_spacing_vertical INTEGER NOT NULL CHECK (image_spacing_vertical >= 0), draw_cut_markers INTEGER NOT NULL CHECK (draw_cut_markers in (TRUE, FALSE)), draw_sharp_corners INTEGER NOT NULL CHECK (draw_sharp_corners in (TRUE, FALSE)) )"""), "INSERT INTO DocumentSettings SELECT *, FALSE FROM DocumentSettings_Old", "DROP TABLE DocumentSettings_Old", "PRAGMA user_version = 5", ]: db.execute(f"{statement}\n") |
Changes to mtg_proxy_printer/model/document_loader.py.
︙ | ︙ | |||
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 | margin_top: int = 0 margin_bottom: int = 0 margin_left: int = 0 margin_right: int = 0 image_spacing_horizontal: int = 0 image_spacing_vertical: int = 0 draw_cut_markers: bool = False @classmethod def create_from_settings(cls): document_settings = mtg_proxy_printer.settings.settings["documents"] return cls( document_settings.getint("paper-height-mm"), document_settings.getint("paper-width-mm"), document_settings.getint("margin-top-mm"), document_settings.getint("margin-bottom-mm"), document_settings.getint("margin-left-mm"), document_settings.getint("margin-right-mm"), document_settings.getint("image-spacing-horizontal-mm"), document_settings.getint("image-spacing-vertical-mm"), document_settings.getboolean("print-cut-marker"), ) def __lt__(self, other): if not isinstance(other, self.__class__): raise TypeError( f"'<' not supported between instances of '{self.__class__.__name__}' and '{other.__class__.__name__}'") return self.compute_page_row_count(PageType.REGULAR) < other.compute_page_card_capacity(PageType.REGULAR) or \ | > > | 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 | margin_top: int = 0 margin_bottom: int = 0 margin_left: int = 0 margin_right: int = 0 image_spacing_horizontal: int = 0 image_spacing_vertical: int = 0 draw_cut_markers: bool = False draw_sharp_corners: bool = False @classmethod def create_from_settings(cls): document_settings = mtg_proxy_printer.settings.settings["documents"] return cls( document_settings.getint("paper-height-mm"), document_settings.getint("paper-width-mm"), document_settings.getint("margin-top-mm"), document_settings.getint("margin-bottom-mm"), document_settings.getint("margin-left-mm"), document_settings.getint("margin-right-mm"), document_settings.getint("image-spacing-horizontal-mm"), document_settings.getint("image-spacing-vertical-mm"), document_settings.getboolean("print-cut-marker"), document_settings.getboolean("print-sharp-corners") ) def __lt__(self, other): if not isinstance(other, self.__class__): raise TypeError( f"'<' not supported between instances of '{self.__class__.__name__}' and '{other.__class__.__name__}'") return self.compute_page_row_count(PageType.REGULAR) < other.compute_page_card_capacity(PageType.REGULAR) or \ |
︙ | ︙ | |||
198 199 200 201 202 203 204 | self.network_error_occurred.emit( f"Error count: {error_count}. Most common error message:\n" f"{self.network_errors_during_load.most_common(1)[0][0]}" ) self.network_errors_during_load.clear() def on_network_error_occurred(self, card: Card, error: str): | | | 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | self.network_error_occurred.emit( f"Error count: {error_count}. Most common error message:\n" f"{self.network_errors_during_load.most_common(1)[0][0]}" ) self.network_errors_during_load.clear() def on_network_error_occurred(self, card: Card, error: str): card.set_image_file(self.image_db.blank_image) self.network_errors_during_load[error] += 1 def load_document(self): self.should_run = True try: unknown_ids, migrated_ids = self._load_document() except AssertionError as e: |
︙ | ︙ | |||
291 292 293 294 295 296 297 | def _read_card_data_from_database(db: sqlite3.Connection, user_version: int) -> DocumentSaveFormat: card_data = [] if user_version == 2: query = textwrap.dedent("""\ SELECT page, slot, scryfall_id, 1 AS is_front FROM Card ORDER BY page ASC, slot ASC""") | | | > > > > > > > > < < < < < < < > > | 293 294 295 296 297 298 299 300 301 302 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 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | def _read_card_data_from_database(db: sqlite3.Connection, user_version: int) -> DocumentSaveFormat: card_data = [] if user_version == 2: query = textwrap.dedent("""\ SELECT page, slot, scryfall_id, 1 AS is_front FROM Card ORDER BY page ASC, slot ASC""") elif user_version in {3, 4, 5}: query = textwrap.dedent("""\ SELECT page, slot, scryfall_id, is_front FROM Card ORDER BY page ASC, slot ASC""") else: raise AssertionError(f"Unknown database schema version: {user_version}") for row_number, row_data in enumerate(db.execute(query)): assert_that(row_data, contains_exactly( all_of(instance_of(int), greater_than_or_equal_to(0)), all_of(instance_of(int), greater_than_or_equal_to(0)), all_of(instance_of(str), matches_regexp(r"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}")), is_in((0, 1)) ), f"Invalid data found in the save data at row {row_number}. Aborting") page, slot, scryfall_id, is_front = row_data card_data.append((page, slot, scryfall_id, bool(is_front))) return card_data @staticmethod def _read_page_layout_data_from_database(db, user_version): if user_version >= 4: document_settings_query = textwrap.dedent(f"""\ SELECT page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, image_spacing_horizontal, image_spacing_vertical, draw_cut_markers, {'1' if user_version == 4 else 'draw_sharp_corners'} FROM DocumentSettings WHERE rowid == 1 """) assert_that( db.execute("SELECT COUNT(*) FROM DocumentSettings").fetchone(), contains_exactly(1), ) settings = PageLayoutSettings(*db.execute(document_settings_query).fetchone()) assert_that( settings, has_properties( page_height=all_of(instance_of(int), greater_than(0)), page_width=all_of(instance_of(int), greater_than(0)), margin_top=all_of(instance_of(int), greater_than_or_equal_to(0)), margin_bottom=all_of(instance_of(int), greater_than_or_equal_to(0)), margin_left=all_of(instance_of(int), greater_than_or_equal_to(0)), margin_right=all_of(instance_of(int), greater_than_or_equal_to(0)), image_spacing_horizontal=all_of(instance_of(int), greater_than_or_equal_to(0)), image_spacing_vertical=all_of(instance_of(int), greater_than_or_equal_to(0)), draw_cut_markers=is_in((0, 1)), draw_sharp_corners=is_in((0, 1)), ), "Document settings contain invalid data or data types" ) assert_that( settings.compute_page_card_capacity(), is_(greater_than_or_equal_to(1)), "Document settings invalid: At least one card has to fit on a page." ) settings.draw_cut_markers = bool(settings.draw_cut_markers) settings.draw_sharp_corners = bool(settings.draw_sharp_corners) else: settings = PageLayoutSettings.create_from_settings() return settings @staticmethod def _validate_database_schema(db_unsafe: sqlite3.Connection) -> int: """ |
︙ | ︙ |
Changes to mtg_proxy_printer/model/imagedb.py.
︙ | ︙ | |||
311 312 313 314 315 316 317 | self.network_error_occurred.emit(self.last_error_message) # Unconditionally forget any previously stored error messages when changing the batch processing state. # This prevents re-raising already reported, previous errors when starting a new batch self.last_error_message = "" self.batch_processing_state_changed.emit(value) def _handle_network_error_during_download(self, card: Card, reason_str: str): | | | 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | self.network_error_occurred.emit(self.last_error_message) # Unconditionally forget any previously stored error messages when changing the batch processing state. # This prevents re-raising already reported, previous errors when starting a new batch self.last_error_message = "" self.batch_processing_state_changed.emit(value) def _handle_network_error_during_download(self, card: Card, reason_str: str): card.set_image_file(self.image_database.blank_image) logger.warning( f"Image download failed for card {card}, reason is \"{reason_str}\". Using blank replacement image.") # Only return the error message for storage, if the queue currently processes a batch job. # Otherwise, it’ll be re-raised if a batch job starts right after a singular request failed. if not self.batch_processing_state: self.network_error_occurred.emit(reason_str) return reason_str |
︙ | ︙ | |||
342 343 344 345 346 347 348 | pixmap = self.image_database.loaded_images[key] except KeyError: logger.debug("Image not in memory, requesting from disk") pixmap = self._fetch_image(card) self.image_database.loaded_images[key] = pixmap self.image_database.images_on_disk.add(key) logger.debug("Image loaded") | | | 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 | pixmap = self.image_database.loaded_images[key] except KeyError: logger.debug("Image not in memory, requesting from disk") pixmap = self._fetch_image(card) self.image_database.loaded_images[key] = pixmap self.image_database.images_on_disk.add(key) logger.debug("Image loaded") card.set_image_file(pixmap) def _fetch_image(self, card: Card) -> QPixmap: key = ImageKey(card.scryfall_id, card.is_front, card.highres_image) cache_file_path = self.image_database.db_path / key.format_relative_path() cache_file_path.parent.mkdir(parents=True, exist_ok=True) pixmap = None if cache_file_path.exists(): |
︙ | ︙ |
Changes to mtg_proxy_printer/resources/ui/page_config_widget.ui.
︙ | ︙ | |||
10 11 12 13 14 15 16 | <height>475</height> </rect> </property> <property name="title"> <string>Default settings for new documents</string> </property> <layout class="QGridLayout" name="gridLayout"> | < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < | | | | | | | | | < < < < < < < < < < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | < < < | | | | | | | | < < < < < < < < < < | | | | > > > | | < < < < | | 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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | <height>475</height> </rect> </property> <property name="title"> <string>Default settings for new documents</string> </property> <layout class="QGridLayout" name="gridLayout"> <item row="10" column="1"> <widget class="QLabel" name="page_height_label"> <property name="text"> <string>Page height</string> </property> <property name="buddy"> <cstring>page_height</cstring> </property> </widget> </item> <item row="17" column="3"> <widget class="QSpinBox" name="margin_right"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="toolTip"> <string><html><head/><body><p>Minimum margin between the right paper border and the page content.</p><p>Most printers have a minimum printing margin of 3 to 5 mm.</p></body></html></string> </property> <property name="suffix"> <string> mm</string> </property> <property name="maximum"> <number>10000</number> </property> </widget> </item> <item row="12" column="3"> <widget class="QSpinBox" name="page_width"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="toolTip"> <string>Default paper width in millimeters</string> </property> <property name="suffix"> <string> mm</string> </property> <property name="maximum"> <number>10000</number> </property> </widget> </item> <item row="0" column="3"> <widget class="QCheckBox" name="draw_cut_markers"> <property name="toolTip"> <string>Enable printing additional lines to aid cutting the printed sheets.</string> </property> <property name="text"> <string>Print cut markers</string> </property> </widget> </item> <item row="10" column="3"> <widget class="QSpinBox" name="page_height"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="toolTip"> <string>Default paper height in millimeters</string> </property> <property name="suffix"> <string> mm</string> </property> <property name="maximum"> <number>10000</number> </property> </widget> </item> <item row="13" column="1"> <widget class="QLabel" name="margin_top_label"> <property name="text"> <string>Top margin</string> </property> <property name="buddy"> <cstring>margin_top</cstring> </property> </widget> </item> <item row="14" column="3"> <widget class="QSpinBox" name="margin_bottom"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="toolTip"> <string><html><head/><body><p>Minimum margin between the bottom paper border and the page content.</p><p>Most printers have a minimum printing margin of 3 to 5 mm.</p></body></html></string> </property> <property name="suffix"> <string> mm</string> </property> <property name="maximum"> <number>10000</number> </property> </widget> </item> <item row="14" column="1"> <widget class="QLabel" name="margin_bottom_label"> <property name="text"> <string>Bottom Margin</string> </property> <property name="buddy"> <cstring>margin_bottom</cstring> </property> </widget> </item> <item row="8" column="1" colspan="3"> <widget class="Line" name="line"> <property name="orientation"> <enum>Qt::Horizontal</enum> </property> </widget> </item> <item row="20" column="3"> <widget class="QLabel" name="page_capacity"> <property name="toolTip"> <string>Number of regular-size cards fitting on each page, based on the page size and spacings configured</string> </property> <property name="text"> <string/> </property> </widget> </item> <item row="17" column="1"> <widget class="QLabel" name="margin_right_label"> <property name="text"> <string>Right margin</string> </property> <property name="buddy"> <cstring>margin_right</cstring> </property> </widget> </item> <item row="19" column="1"> <widget class="QLabel" name="image_spacing_vertical_label"> <property name="text"> <string>Vertical image spacing</string> </property> <property name="buddy"> <cstring>image_spacing_vertical</cstring> </property> </widget> </item> <item row="16" column="3"> <widget class="QSpinBox" name="margin_left"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="toolTip"> <string><html><head/><body><p>Minimum margin between the left paper border and the page content.</p><p>Most printers have a minimum printing margin of 3 to 5 mm.</p></body></html></string> </property> <property name="suffix"> <string> mm</string> </property> <property name="maximum"> <number>10000</number> </property> </widget> </item> <item row="18" column="1"> <widget class="QLabel" name="image_spacing_horizontal_label"> <property name="text"> <string>Horizontal image spacing</string> </property> <property name="buddy"> <cstring>image_spacing_horizontal</cstring> </property> </widget> </item> <item row="20" column="1"> <widget class="QLabel" name="page_capacity_label"> <property name="text"> <string>Resulting page capacity:</string> </property> </widget> </item> <item row="13" column="3"> <widget class="QSpinBox" name="margin_top"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="toolTip"> <string><html><head/><body><p>Minimum margin between the top paper border and the page content.</p><p>Most printers have a minimum printing margin of 3 to 5 mm.</p></body></html></string> </property> <property name="suffix"> <string> mm</string> </property> <property name="maximum"> <number>10000</number> </property> </widget> </item> <item row="19" column="3"> <widget class="QSpinBox" name="image_spacing_vertical"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="toolTip"> <string><html><head/><body><p>Space above and below images. Images are this many millimeters apart.</p><p>If set to zero, you only need one cut to separate two images, otherwise you need two cuts but require less precision hitting the exact middle.</p></body></html></string> </property> <property name="suffix"> <string> mm</string> </property> <property name="maximum"> <number>10000</number> </property> </widget> </item> <item row="16" column="1"> <widget class="QLabel" name="margin_left_label"> <property name="text"> <string>Left margin</string> </property> <property name="buddy"> <cstring>margin_left</cstring> </property> </widget> </item> <item row="18" column="3"> <widget class="QSpinBox" name="image_spacing_horizontal"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="toolTip"> <string><html><head/><body><p>Space left and right of images. Images are this many millimeters apart.</p><p>If set to zero, you only need one cut to separate two images, otherwise you need two cuts but require less precision hitting the exact middle.</p></body></html></string> </property> <property name="suffix"> <string> mm</string> </property> <property name="maximum"> <number>10000</number> </property> </widget> </item> <item row="12" column="1"> <widget class="QLabel" name="page_width_label"> <property name="text"> <string>Page width</string> </property> <property name="buddy"> <cstring>page_width</cstring> </property> </widget> </item> <item row="1" column="3"> <widget class="QCheckBox" name="draw_sharp_corners"> <property name="text"> <string>Draw 90° card corners, instead of round ones</string> </property> </widget> </item> </layout> </widget> <tabstops> <tabstop>draw_cut_markers</tabstop> |
︙ | ︙ |
Changes to mtg_proxy_printer/settings.py.
︙ | ︙ | |||
49 50 51 52 53 54 55 56 57 58 59 60 61 62 | VERSION_CHECK_RE = re.compile( # sourced from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)" r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][\da-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][\da-zA-Z-]*))*))?" r"(?:\+(?P<buildmetadata>[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?$" ) DEFAULT_SETTINGS["images"] = { "preferred-language": "en", "automatically-add-opposing-faces": "True", } DEFAULT_SETTINGS["card-filter"] = { "hide-cards-depicting-racism": "True", "hide-cards-without-images": "True", | > > > > > > > > > | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | VERSION_CHECK_RE = re.compile( # sourced from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)" r"(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][\da-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][\da-zA-Z-]*))*))?" r"(?:\+(?P<buildmetadata>[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?$" ) # Below are the default application settings. How to define new ones: # - Add a key-value pair (String keys and values only) to a section or add a new section # - If adding a new section, also add a validator function for that section. # - Add the new key to the validator of the section it’s in. The validator has to check that the value can be properly # cast into the expected type and perform a value range check. # - Add the option to the Settings window UI # - Wire up save and load functionality for the new key in the Settings UI # - The Settings GUI class has to also do a value range check. DEFAULT_SETTINGS["images"] = { "preferred-language": "en", "automatically-add-opposing-faces": "True", } DEFAULT_SETTINGS["card-filter"] = { "hide-cards-depicting-racism": "True", "hide-cards-without-images": "True", |
︙ | ︙ | |||
84 85 86 87 88 89 90 91 92 93 94 95 96 97 | "margin-bottom-mm": "10", "margin-left-mm": "7", "margin-right-mm": "7", "image-spacing-horizontal-mm": "0", "image-spacing-vertical-mm": "0", "print-cut-marker": "False", "pdf-page-count-limit": "0", } DEFAULT_SETTINGS["default-filesystem-paths"] = { "document-save-path": QStandardPaths.locate(QStandardPaths.DocumentsLocation, "", QStandardPaths.LocateDirectory), "pdf-export-path": QStandardPaths.locate(QStandardPaths.DocumentsLocation, "", QStandardPaths.LocateDirectory), "deck-list-search-path": QStandardPaths.locate(QStandardPaths.DownloadLocation, "", QStandardPaths.LocateDirectory), } DEFAULT_SETTINGS["gui"] = { | > | 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | "margin-bottom-mm": "10", "margin-left-mm": "7", "margin-right-mm": "7", "image-spacing-horizontal-mm": "0", "image-spacing-vertical-mm": "0", "print-cut-marker": "False", "pdf-page-count-limit": "0", "print-sharp-corners": "False", } DEFAULT_SETTINGS["default-filesystem-paths"] = { "document-save-path": QStandardPaths.locate(QStandardPaths.DocumentsLocation, "", QStandardPaths.LocateDirectory), "pdf-export-path": QStandardPaths.locate(QStandardPaths.DocumentsLocation, "", QStandardPaths.LocateDirectory), "deck-list-search-path": QStandardPaths.locate(QStandardPaths.DownloadLocation, "", QStandardPaths.LocateDirectory), } DEFAULT_SETTINGS["gui"] = { |
︙ | ︙ | |||
189 190 191 192 193 194 195 | _restore_default(section, defaults, "preferred-language") def _validate_documents_section(settings: configparser.ConfigParser, section_name: str = "documents"): sizes: mtg_proxy_printer.units_and_sizes.CardSize = mtg_proxy_printer.units_and_sizes.CardSizes.OVERSIZED.value section = settings[section_name] defaults = DEFAULT_SETTINGS[section_name] | | | > | | | 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | _restore_default(section, defaults, "preferred-language") def _validate_documents_section(settings: configparser.ConfigParser, section_name: str = "documents"): sizes: mtg_proxy_printer.units_and_sizes.CardSize = mtg_proxy_printer.units_and_sizes.CardSizes.OVERSIZED.value section = settings[section_name] defaults = DEFAULT_SETTINGS[section_name] boolean_settings = {"print-cut-marker", "print-sharp-corners"} # Check syntax for key in section.keys(): if key in boolean_settings: _validate_boolean(section, defaults, key) else: _validate_non_negative_int(section, defaults, key) # Check some semantic properties available_height = section.getint("paper-height-mm") - \ (section.getint("margin-top-mm") + section.getint("margin-bottom-mm")) available_width = section.getint("paper-width-mm") - \ (section.getint("margin-left-mm") + section.getint("margin-right-mm")) if available_height < sizes.height: |
︙ | ︙ |
Changes to mtg_proxy_printer/ui/page_config_widget.py.
︙ | ︙ | |||
51 52 53 54 55 56 57 58 59 60 61 62 63 64 | self.margin_right.valueChanged[int].connect(partial(setattr, page_layout, "margin_right")) self.image_spacing_horizontal.valueChanged[int].connect( partial(setattr, page_layout, "image_spacing_horizontal")) self.image_spacing_vertical.valueChanged[int].connect(partial(setattr, page_layout, "image_spacing_vertical")) self.draw_cut_markers: QCheckBox self.draw_cut_markers.stateChanged.connect( lambda new: setattr(page_layout, "draw_cut_markers", new == Qt.Checked)) return page_layout @Slot() def page_layout_setting_changed(self): """ Recomputes and updates the page capacity value, whenever any page layout widget changes. Qt Signal/Slot connections from editor widgets valueChanged[int] signals are defined in the UI file. | > > > | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | self.margin_right.valueChanged[int].connect(partial(setattr, page_layout, "margin_right")) self.image_spacing_horizontal.valueChanged[int].connect( partial(setattr, page_layout, "image_spacing_horizontal")) self.image_spacing_vertical.valueChanged[int].connect(partial(setattr, page_layout, "image_spacing_vertical")) self.draw_cut_markers: QCheckBox self.draw_cut_markers.stateChanged.connect( lambda new: setattr(page_layout, "draw_cut_markers", new == Qt.Checked)) self.draw_sharp_corners: QCheckBox self.draw_sharp_corners.stateChanged.connect( lambda new: setattr(page_layout, "draw_sharp_corners", new == Qt.Checked)) return page_layout @Slot() def page_layout_setting_changed(self): """ Recomputes and updates the page capacity value, whenever any page layout widget changes. Qt Signal/Slot connections from editor widgets valueChanged[int] signals are defined in the UI file. |
︙ | ︙ | |||
81 82 83 84 85 86 87 | self.page_width: QSpinBox self.page_height.setMinimum(min_page_height) self.page_width.setMinimum(min_page_width) def load_document_settings_from_config(self, settings: configparser.ConfigParser): logger.debug(f"About to load document settings from the global settings") document_section = settings["documents"] | < | | > | | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | self.page_width: QSpinBox self.page_height.setMinimum(min_page_height) self.page_width.setMinimum(min_page_width) def load_document_settings_from_config(self, settings: configparser.ConfigParser): logger.debug(f"About to load document settings from the global settings") document_section = settings["documents"] for spinbox, setting in self._get_integer_settings_widgets(): spinbox.setValue(document_section.getint(setting)) for checkbox, setting in self._get_boolean_settings_widgets(): checkbox.setChecked(document_section.getboolean(setting)) logger.debug(f"Loading from settings finished") def load_from_page_layout(self, other: PageLayoutSettings): """Loads the page layout from another PageLayoutSettings instance""" logger.debug(f"About to load document settings from a document instance") layout = self.page_layout for key in layout.__annotations__.keys(): |
︙ | ︙ | |||
107 108 109 110 111 112 113 | self.validate_paper_size_settings() self.page_layout_setting_changed() logger.debug(f"Loading from document settings finished") def save_document_settings_to_config(self): logger.info("About to save document settings to the global settings") documents_section = mtg_proxy_printer.settings.settings["documents"] | < | | > | | > > > > > > > | 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 | self.validate_paper_size_settings() self.page_layout_setting_changed() logger.debug(f"Loading from document settings finished") def save_document_settings_to_config(self): logger.info("About to save document settings to the global settings") documents_section = mtg_proxy_printer.settings.settings["documents"] for spinbox, setting in self._get_integer_settings_widgets(): documents_section[setting] = str(spinbox.value()) for checkbox, setting in self._get_boolean_settings_widgets(): documents_section[setting] = str(checkbox.isChecked()) logger.debug("Saving done.") def _get_integer_settings_widgets(self): widgets_with_settings: typing.List[typing.Tuple[QSpinBox, str]] = [ (self.page_height, "paper-height-mm"), (self.page_width, "paper-width-mm"), (self.margin_top, "margin-top-mm"), (self.margin_bottom, "margin-bottom-mm"), (self.margin_left, "margin-left-mm"), (self.margin_right, "margin-right-mm"), (self.image_spacing_horizontal, "image-spacing-horizontal-mm"), (self.image_spacing_vertical, "image-spacing-vertical-mm"), ] return widgets_with_settings def _get_boolean_settings_widgets(self): widgets_with_settings: typing.List[typing.Tuple[QCheckBox, str]] = [ (self.draw_cut_markers, "print-cut-marker"), (self.draw_sharp_corners, "print-sharp-corners"), ] return widgets_with_settings |
Changes to mtg_proxy_printer/ui/page_renderer.py.
︙ | ︙ | |||
20 21 22 23 24 25 26 27 28 29 30 31 32 33 | pyqtSignal as Signal, QEvent from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QWidget, QAction from PyQt5.QtGui import QColor, QPixmap, QWheelEvent, QKeySequence, QPalette, QBrush, QResizeEvent import pint from mtg_proxy_printer.units_and_sizes import PageType, CardSizes, CardSize, unit_registry, DPI from mtg_proxy_printer.model.document import Document from mtg_proxy_printer.model.card_list import PageColumns from mtg_proxy_printer.logger import get_logger logger = get_logger(__name__) del get_logger __all__ = [ | > | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | pyqtSignal as Signal, QEvent from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QWidget, QAction from PyQt5.QtGui import QColor, QPixmap, QWheelEvent, QKeySequence, QPalette, QBrush, QResizeEvent import pint from mtg_proxy_printer.units_and_sizes import PageType, CardSizes, CardSize, unit_registry, DPI from mtg_proxy_printer.model.document import Document from mtg_proxy_printer.model.carddb import Card, CardCorner from mtg_proxy_printer.model.card_list import PageColumns from mtg_proxy_printer.logger import get_logger logger = get_logger(__name__) del get_logger __all__ = [ |
︙ | ︙ | |||
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | self.draw_card(row) def draw_card(self, row: int): index = self.selected_page.child(row, PageColumns.Image) position = self._compute_position_for_image(index) image: QPixmap = index.data(Qt.DisplayRole) if image is not None: pixmap = self.addPixmap(image) pixmap.setTransformationMode(Qt.SmoothTransformation) pixmap.setPos(position) @Slot(QModelIndex) def on_page_type_changed(self, page: QModelIndex): if page.row() == self.selected_page.row(): self.redraw() def on_data_changed(self, top_left: QModelIndex, bottom_right: QModelIndex, roles: typing.List[Qt.ItemDataRole]): if top_left.parent().row() == self.selected_page.row() and Qt.DisplayRole in roles: | > > > > > > > > > > > > > > > > > > > > | 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 | self.draw_card(row) def draw_card(self, row: int): index = self.selected_page.child(row, PageColumns.Image) position = self._compute_position_for_image(index) image: QPixmap = index.data(Qt.DisplayRole) if image is not None: if self.document.page_layout.draw_sharp_corners: self._draw_corners(index, position) pixmap = self.addPixmap(image) pixmap.setTransformationMode(Qt.SmoothTransformation) pixmap.setPos(position) def _draw_corners(self, index: QModelIndex, position: QPointF): card: Card = index.internalPointer().card image = card.image_file corner_size = QSizeF(50, 50) # Needs to offset the corner position by some half pixels to not overlap self.addRect( QRectF(position + QPointF(0.5, 0.5), corner_size), card.corner_color(CardCorner.TOP_LEFT), card.corner_color(CardCorner.TOP_LEFT)) self.addRect( QRectF(position + image.rect().topRight() - QPointF(49.5, -0.5), corner_size), card.corner_color(CardCorner.TOP_RIGHT), card.corner_color(CardCorner.TOP_RIGHT)) self.addRect( QRectF(position + image.rect().bottomLeft() - QPointF(-0.5, 49.5), corner_size), card.corner_color(CardCorner.BOTTOM_LEFT), card.corner_color(CardCorner.BOTTOM_LEFT)) self.addRect( QRectF(position + image.rect().bottomRight() - QPointF(49.5, 49.5), corner_size), card.corner_color(CardCorner.BOTTOM_RIGHT), card.corner_color(CardCorner.BOTTOM_RIGHT)) @Slot(QModelIndex) def on_page_type_changed(self, page: QModelIndex): if page.row() == self.selected_page.row(): self.redraw() def on_data_changed(self, top_left: QModelIndex, bottom_right: QModelIndex, roles: typing.List[Qt.ItemDataRole]): if top_left.parent().row() == self.selected_page.row() and Qt.DisplayRole in roles: |
︙ | ︙ | |||
201 202 203 204 205 206 207 | spacing_horizontal = page_layout.image_spacing_horizontal x_pos = page_layout.margin_left + column * (card_size.width + spacing_horizontal) y_pos = page_layout.margin_top + row * (card_size.height + spacing_vertical) scaling_horizontal = self.width() / page_layout.page_width scaling_vertical = self.height() / page_layout.page_height return QPointF( | | | | 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | spacing_horizontal = page_layout.image_spacing_horizontal x_pos = page_layout.margin_left + column * (card_size.width + spacing_horizontal) y_pos = page_layout.margin_top + row * (card_size.height + spacing_vertical) scaling_horizontal = self.width() / page_layout.page_width scaling_vertical = self.height() / page_layout.page_height return QPointF( x_pos * scaling_horizontal + 0.5*column, y_pos * scaling_vertical + 0.5*row, ) def _draw_cut_markers(self): """Draws the optional cut markers that extend to the paper border""" page_type: PageType = self.selected_page.data(Qt.EditRole).page_type() if page_type == PageType.MIXED: logger.warning("Not drawing cut markers for page with mixed image sizes") |
︙ | ︙ | |||
225 226 227 228 229 230 231 | def _draw_vertical_markers(self, line_color: QColor, card_size: CardSize): page_layout = self.document.page_layout scaling_horizontal = self.width() / page_layout.page_width column_count = page_layout.compute_page_column_count(page_layout) if not page_layout.image_spacing_horizontal: column_count += 1 for column in range(column_count): | | | | | 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | def _draw_vertical_markers(self, line_color: QColor, card_size: CardSize): page_layout = self.document.page_layout scaling_horizontal = self.width() / page_layout.page_width column_count = page_layout.compute_page_column_count(page_layout) if not page_layout.image_spacing_horizontal: column_count += 1 for column in range(column_count): column_px = 0.5 * column + scaling_horizontal * ( page_layout.margin_left + column * (card_size.width + page_layout.image_spacing_horizontal) ) self._draw_vertical_line(column_px, line_color) if page_layout.image_spacing_horizontal: offset = 1 + card_size.width * scaling_horizontal self._draw_vertical_line(column_px + offset, line_color) logger.debug(f"Vertical cut markers drawn") def _draw_horizontal_markers(self, line_color: QColor, card_size: CardSize): page_layout = self.document.page_layout scaling_vertical = self.height() / page_layout.page_height row_count = page_layout.compute_page_row_count(page_layout) if not page_layout.image_spacing_vertical: row_count += 1 for row in range(row_count): row_px = 0.5 * row + scaling_vertical * ( page_layout.margin_top + row * (card_size.height + page_layout.image_spacing_vertical) ) self._draw_horizontal_line(row_px, line_color) if page_layout.image_spacing_vertical: offset = 0.5 + card_size.height * scaling_vertical self._draw_horizontal_line(row_px + offset, line_color) logger.debug(f"Horizontal cut markers drawn") def _draw_vertical_line(self, column_px: int, line_color: QColor): self.addLine(column_px, 0, column_px, self.height(), line_color) def _draw_horizontal_line(self, row_px: int, line_color: QColor): |
︙ | ︙ |
Changes to tests/conftest.py.
︙ | ︙ | |||
44 45 46 47 48 49 50 | card_db.db.execute("PRAGMA reverse_unordered_selects = TRUE") return card_db @pytest.fixture(params=[False, True]) def empty_save_database(request) -> sqlite3.Connection: db = mtg_proxy_printer.sqlite_helpers.open_database( | | | 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | card_db.db.execute("PRAGMA reverse_unordered_selects = TRUE") return card_db @pytest.fixture(params=[False, True]) def empty_save_database(request) -> sqlite3.Connection: db = mtg_proxy_printer.sqlite_helpers.open_database( ":memory:", "document-v5", CardDatabase.MIN_SUPPORTED_SQLITE_VERSION, check_same_thread=False) if request.param: db.execute("PRAGMA reverse_unordered_selects = TRUE") return db @pytest.fixture() def image_db(): |
︙ | ︙ |
Changes to tests/test_document.py.
︙ | ︙ | |||
229 230 231 232 233 234 235 | assert_that(document.pages[1].page_type(), is_(PageType.OVERSIZED)) assert_that(document.pages[2].page_type(), is_(PageType.REGULAR)) assert_that(document.pages[3].page_type(), is_(PageType.OVERSIZED)) assert_that(document.pages[4].page_type(), is_(PageType.MIXED)) assert_that(document.pages[5].page_type(), is_(PageType.UNDETERMINED)) | | | 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | assert_that(document.pages[1].page_type(), is_(PageType.OVERSIZED)) assert_that(document.pages[2].page_type(), is_(PageType.REGULAR)) assert_that(document.pages[3].page_type(), is_(PageType.OVERSIZED)) assert_that(document.pages[4].page_type(), is_(PageType.MIXED)) assert_that(document.pages[5].page_type(), is_(PageType.UNDETERMINED)) @pytest.mark.parametrize("source_version", [2, 3, 4]) def test_save_migration(document: Document, source_version: int): """Tests migration of existing saves to the newest schema revision on save.""" card = document.card_db.get_card_with_scryfall_id("0000579f-7b35-4ed3-b44c-db2a538066fe", True) document.add_card(card, document.total_cards_per_page) with TemporaryDirectory() as temp_dir: document.save_file_path = _create_save_file(pathlib.Path(temp_dir), source_version) document.save_to_disk() |
︙ | ︙ | |||
298 299 300 301 302 303 304 | def _validate_database_schema(db_path: pathlib.Path): """ Validates the database schema of the user-provided file against a known-good schema. :raises AssertionError: If the provided file contains an invalid schema :returns: Database schema version """ | | | 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | def _validate_database_schema(db_path: pathlib.Path): """ Validates the database schema of the user-provided file against a known-good schema. :raises AssertionError: If the provided file contains an invalid schema :returns: Database schema version """ target_schema_version = 5 db_unsafe = open_database( db_path, f"document-v{target_schema_version}", DocumentLoader.MIN_SUPPORTED_SQLITE_VERSION) if db_unsafe.execute("PRAGMA application_id").fetchone()[0] != 41325044: raise AssertionError("Not an MTGProxyPrinter save file!") user_schema_version = db_unsafe.execute("PRAGMA user_version").fetchone()[0] assert_that(user_schema_version, is_(equal_to(target_schema_version))) db_known_good = create_in_memory_database( |
︙ | ︙ |
Changes to tests/test_document_loader.py.
︙ | ︙ | |||
27 28 29 30 31 32 33 | import mtg_proxy_printer.model.document_loader from mtg_proxy_printer.units_and_sizes import PageType import mtg_proxy_printer.model.document import mtg_proxy_printer.sqlite_helpers | | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import mtg_proxy_printer.model.document_loader from mtg_proxy_printer.units_and_sizes import PageType import mtg_proxy_printer.model.document import mtg_proxy_printer.sqlite_helpers @pytest.mark.parametrize("version", [-1, 0, 1, 6, 7]) def test_unknown_save_version_raises_exception(empty_save_database: sqlite3.Connection, version: int): empty_save_database.execute(f"PRAGMA user_version = {version};") assert_that(empty_save_database.execute("PRAGMA user_version").fetchone()[0], is_(version)) with unittest.mock.patch("mtg_proxy_printer.model.document.mtg_proxy_printer.sqlite_helpers.open_database") as mock: mock.return_value = empty_save_database assert_that( calling(mtg_proxy_printer.model.document_loader.DocumentLoader.Worker._read_data_from_save_path).with_args( |
︙ | ︙ | |||
76 77 78 79 80 81 82 | ) assert_that(page_layout.compute_page_card_capacity(PageType.OVERSIZED), is_(greater_than_or_equal_to(1))) empty_save_database.execute( textwrap.dedent("""\ INSERT INTO DocumentSettings (rowid, page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, | | | | 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | ) assert_that(page_layout.compute_page_card_capacity(PageType.OVERSIZED), is_(greater_than_or_equal_to(1))) empty_save_database.execute( textwrap.dedent("""\ INSERT INTO DocumentSettings (rowid, page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, image_spacing_horizontal, image_spacing_vertical, draw_cut_markers, draw_sharp_corners) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """), dataclasses.astuple(page_layout)) loader = document.loader save_path = pathlib.Path("/tmp/invalid.mtgproxies") with unittest.mock.patch("mtg_proxy_printer.model.document.mtg_proxy_printer.sqlite_helpers.open_database") as mock: mock.return_value = empty_save_database with qtbot.waitSignal(loader.loading_state_changed, timeout=1000, check_params_cb=lambda value: not value), \ |
︙ | ︙ | |||
119 120 121 122 123 124 125 | (1, 2, 1, "650722b4-d72b-4745-a1a5-00a34836282b"), ] ) empty_save_database.execute( textwrap.dedent("""\ INSERT INTO DocumentSettings (rowid, page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, | | | | 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | (1, 2, 1, "650722b4-d72b-4745-a1a5-00a34836282b"), ] ) empty_save_database.execute( textwrap.dedent("""\ INSERT INTO DocumentSettings (rowid, page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, image_spacing_horizontal, image_spacing_vertical, draw_cut_markers, draw_sharp_corners) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """), dataclasses.astuple(mtg_proxy_printer.model.document.PageLayoutSettings.create_from_settings())) loader = document.loader save_path = pathlib.Path("/tmp/invalid.mtgproxies") with unittest.mock.patch("mtg_proxy_printer.model.document.mtg_proxy_printer.sqlite_helpers.open_database") as mock: mock.return_value = empty_save_database with qtbot.waitSignal(loader.loading_state_changed, timeout=1000, check_params_cb=lambda value: not value), \ |
︙ | ︙ | |||
212 213 214 215 216 217 218 | empty_save_database: sqlite3.Connection): empty_save_database.execute("DROP TABLE DocumentSettings") # LIMIT clause in the definition below is a safety measure. empty_save_database.execute(textwrap.dedent("""\ CREATE VIEW DocumentSettings ( rowid, page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, | | | | | | 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | empty_save_database: sqlite3.Connection): empty_save_database.execute("DROP TABLE DocumentSettings") # LIMIT clause in the definition below is a safety measure. empty_save_database.execute(textwrap.dedent("""\ CREATE VIEW DocumentSettings ( rowid, page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, image_spacing_horizontal, image_spacing_vertical, draw_cut_markers, draw_sharp_corners) AS WITH RECURSIVE settings_gen ( rowid, page_height, page_width, margin_top, margin_bottom, margin_left, margin_right, image_spacing_horizontal, image_spacing_vertical, draw_cut_markers, draw_sharp_corners ) AS ( SELECT 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1 UNION ALL SELECT 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1 FROM settings_gen LIMIT 100000 ) SELECT * FROM settings_gen """)) loader = document.loader with unittest.mock.patch("mtg_proxy_printer.model.document.mtg_proxy_printer.sqlite_helpers.open_database") as mock: |
︙ | ︙ |