Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewritten printing logic and intelligent corner-filling #19

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install package
run: |
python -m pip install .[dev]
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
# You are now able to use PDM in your workflow
- name: Install packages
run: pdm install -G test --skip=post_install
- name: Test with pytest
run: |
python -m pytest tests/
pdm test
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,8 @@ dmypy.json

# Pyre type checker
.pyre/

# MTG Cache
.cache_mtg/
tests/outputs/
tests/.test_cache/
48 changes: 16 additions & 32 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,30 @@ repos:
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: end-of-file-fixer
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.0.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [] # optional: list of Conventional Commits types to allow e.g. [feat, fix, ci, chore, test]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args:
- "--py39-plus"
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.1
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.9
hooks:
- id: flake8
additional_dependencies:
- flake8-absolute-import
- flake8-annotations
- flake8-builtins
- flake8-comprehensions
- flake8-docstrings
- flake8-future-annotations
- flake8-no-implicit-concat
- flake8-print
- flake8-requirements
- flake8-simplify
- flake8-use-fstring
- flake8-use-pathlib
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
hooks:
- id: prettier
args:
- "--tab-width"
- "2"
# Run the linter.
- id: ruff
types_or: [python, pyi]
args: [--fix]
# Run the formatter.
- id: ruff-format
types_or: [python, pyi]
- repo: https://github.com/shssoichiro/oxipng
rev: v9.0.0
rev: v9.1.1
hooks:
- id: oxipng
args:
Expand Down
106 changes: 56 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,67 +20,83 @@ Create a high quality printable PDF from your decklist or a list of cards you wa
The Arena format is recommended, as it allows you to keep the same prints when moving decklists between multiple tools.
There are even cases (i.e. tokens) where the name alone is not sufficient to uniquely specify a card.
The Arena format helps in these case, as set and collector number are unique identifiers.
However, as tools often only work with one of these formats, `mtg-proxies` is a flexible as possible, even supporting mixed mode.
This is especially when you are making quick additions to a decklist and don't want to search for set and collector numbers.
However, as tools often only work with one of these formats, `mtg-proxies` is as flexible as possible, even supporting mixed mode.
This is especially useful when you are making quick additions to a decklist and don't want to search for set and collector numbers.
The `convert.py` tool can be used to convert decklists between the two formats.

- **Sanity checks and recommender engine**
`mtg-proxies` warns you if you attempt to print a low-resolution scan and is able to offer alternatives.
The `convert.py` tool can automatically selects the best print for each card in a decklist with high accuracy, eliminating the need to manually select good prints.
The `convert.py` tool can automatically select the best print for each card in a decklist with high accuracy, eliminating the need to manually select good prints.

- **Token support**
The `tokens.py` tool appends the tokens created by the cards in a decklist to it, so you don't miss one accidentally. Caveat: This only works when Scryfall has the data on associated tokens. This is the case for cards printed or reprinted since Tenth Edition.

- **ManaStack and Archidekt integration**
Directly use ManaStack and Archidekt deck ids as input for many functions instead of local decklist files.

Decks on Archidekt must be set to public to be read.
Decks on Archidekt must be set as public to be read.

## Usage

1. Clone or download this repo.

```bash
git clone https://github.com/DiddiZ/mtg-proxies.git
cd mtg-proxies
```
```bash
git clone https://github.com/DiddiZ/mtg-proxies.git
cd mtg-proxies
```

2. Install requirements. Requires at least [Python 3.9](https://www.python.org/downloads/).

```bash
# On Linux, use `python3` instead of `python`
python -m pip install -e .
```

You can also use a [virtual environment](https://docs.python.org/3/library/venv.html).
You can use pip or PDM to install the dependencies.

- Install using pip (you can use a [virtual environment](https://docs.python.org/3/library/venv.html)).
```bash
# On Linux, use `python3` instead of `python`
python -m venv .venv # Create a virtual environment
source .venv/bin/activate # Activate the virtual environment
# on Windows, run .venv\Scripts\activate.bat or .venv\Scripts\Activate.ps1
python -m pip install -e . # Install in editable mode
```
- Install using PDM (recommended)
```bash
pdm install # creates a virtual env. and installs dependencies on its own
```

3. (Optional) Prepare your decklist in MtG Arena format.
This is not required, but recommended as it allows for more control over the process.

```txt
COUNT FULL_NAME (SET) COLLECTOR_NUMBER
```
```txt
COUNT FULL_NAME (SET) COLLECTOR_NUMBER
```

E.g.:
E.g.:

```txt
1 Alela, Artful Provocateur (ELD) 324
1 Korvold, Fae-Cursed King (ELD) 329
1 Liliana, Dreadhorde General (WAR) 97
1 Murderous Rider // Swift End (ELD) 287
```
```txt
1 Alela, Artful Provocateur (ELD) 324
1 Korvold, Fae-Cursed King (ELD) 329
1 Liliana, Dreadhorde General (WAR) 97
1 Murderous Rider // Swift End (ELD) 287
```

Or use the `convert.py` tool to convert a plain decklist to Arena format:
Or use the `convert.py` tool to convert a plain decklist to Arena format:

```bash
python convert.py examples/decklist_text.txt examples/decklist.txt
```
```bash
python convert.py examples/decklist_text.txt examples/decklist.txt
```

4. Create a PDF file.
4. Assemble your proxies into a document.

```bash
python print.py examples/decklist.txt decklist.pdf
```
To find out what options are available, run:

```bash
pdm run python print.py --help
```

There are two commands available: `pdf` and `image`.

```python
pdm run python print.py image --help
pdm run python print.py pdf --help
```

Examples:

Expand All @@ -103,25 +119,15 @@ python -m pip install -e .
### print

```txt
pipenv run python print.py [-h] [--dpi DPI] decklist outfile
pdm run python print.py --help
Usage: print.py [OPTIONS] COMMAND [ARGS]...

Prepare a decklist for printing.
Options:
--help Show this message and exit.

positional arguments:
decklist_spec path to a decklist in text/arena format, or manastack:{manastack_id}, or archidekt:{archidekt_id}
outfile output file. Supports pdf, png and jpg.

optional arguments:
-h, --help show this help message and exit
--dpi DPI dpi of output file (default: 300)
--paper WIDTHxHEIGHT paper size in inches or preconfigured format (default: a4)
--scale FLOAT scaling factor for printed cards (default: 1.0)
--border_crop PIXELS how much to crop inner borders of printed cards (default: 14)
--background COLOR background color, either by name or by hex code (e.g. black or "#ff0000", default: None)
--cropmarks, --no-cropmarks
add crop marks (default: True)
--faces {all,front,back}
which faces to print (default: all)
Commands:
image This command generates an image file at OUTPUT_FILE with the...
pdf This command generates a PDF document at OUTPUT_FILE with the...
```

### convert
Expand Down
2 changes: 1 addition & 1 deletion deck_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import matplotlib.pyplot as plt

import scryfall
from mtgproxies import scryfall
from mtgproxies.cli import parse_decklist_spec

if __name__ == "__main__":
Expand Down
3 changes: 0 additions & 3 deletions mtgproxies/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from mtgproxies.print_cards import print_cards_fpdf, print_cards_matplotlib
from mtgproxies.scans import fetch_scans_scryfall

__all__ = [
"fetch_scans_scryfall",
"print_cards_matplotlib",
"print_cards_fpdf",
]
5 changes: 3 additions & 2 deletions mtgproxies/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
from mtgproxies.decklists.decklist import Decklist


def parse_decklist_spec(decklist_spec: str, warn_levels=("ERROR", "WARNING", "COSMETIC")) -> Decklist:
def parse_decklist_spec(decklist_spec: str, cache_dir: Path, warn_levels=("ERROR", "WARNING", "COSMETIC")) -> Decklist:
"""Attempt to parse a decklist from different locations.

Args:
decklist_spec: File path or ManaStack id
cache_dir: Directory with cached files
warn_levels: Levels of warnings to show
"""
print("Parsing decklist ...")
if Path(decklist_spec).is_file(): # Decklist is file
decklist, ok, warnings = parse_decklist(decklist_spec)
decklist, ok, warnings = parse_decklist(decklist_spec, cache_dir=cache_dir)
elif decklist_spec.lower().startswith("manastack:") and decklist_spec.split(":")[-1].isdigit():
# Decklist on Manastack
manastack_id = decklist_spec.split(":")[-1]
Expand Down
14 changes: 10 additions & 4 deletions mtgproxies/decklists/archidekt/archidekt.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
from __future__ import annotations

from pathlib import Path

import requests

from mtgproxies.decklists import Decklist
from mtgproxies.decklists.sanitizing import validate_card_name, validate_print


def parse_decklist(archidekt_id: str) -> tuple[Decklist, bool, list]:
def parse_decklist(archidekt_id: str, cache_dir: Path, zones: list[str] = None) -> tuple[Decklist, bool, list]:
"""Parse a decklist from manastack.

Args:
archidekt_id: Deck list id as shown in the deckbuilder URL
cache_dir: Directory to store cached card data
zones: List of zones to include. Available are: `mainboard`, `commander`, `sideboard` and `maybeboard`
"""
decklist = Decklist()
warnings = []
ok = True

if zones is not None:
raise NotImplementedError("Zones are not implemented for Archidekt decklists")

r = requests.get(f"https://archidekt.com/api/decks/{archidekt_id}/")
if r.status_code != 200:
raise (ValueError(f"Archidekt returned statuscode {r.status_code}"))
Expand All @@ -31,19 +37,19 @@ def parse_decklist(archidekt_id: str) -> tuple[Decklist, bool, list]:
card_name = item["card"]["oracleCard"]["name"]
set_id = item["card"]["edition"]["editioncode"]
collector_number = item["card"]["collectorNumber"]
if len(item["categories"]) > 0 and item["categories"][0] not in in_deck:
if item["categories"] is not None and len(item["categories"]) > 0 and item["categories"][0] not in in_deck:
continue

# Validate card name
card_name, warnings_name = validate_card_name(card_name)
card_name, warnings_name = validate_card_name(card_name, cache_dir=cache_dir)
if card_name is None:
decklist.append_comment(card_name)
warnings.extend([(decklist.entries[-1], level, msg) for level, msg in warnings_name])
ok = False
continue

# Validate card print
card, warnings_print = validate_print(card_name, set_id, collector_number)
card, warnings_print = validate_print(card_name, set_id, collector_number, cache_dir=cache_dir)

decklist.append_card(count, card)
warnings.extend([(decklist.entries[-1], level, msg) for level, msg in warnings_name + warnings_print])
Expand Down
14 changes: 7 additions & 7 deletions mtgproxies/decklists/decklist.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pathlib import Path
from typing import Any

import scryfall
from mtgproxies import scryfall
from mtgproxies.decklists.sanitizing import validate_card_name, validate_print


Expand Down Expand Up @@ -117,7 +117,7 @@ def from_scryfall_ids(card_ids) -> Decklist:
return decklist


def parse_decklist(filepath: str | Path) -> tuple[Decklist, bool, list]:
def parse_decklist(filepath: str | Path, cache_dir: Path) -> tuple[Decklist, bool, list]:
"""Parse card information from a decklist in text or MtG Arena (or mixed) format.

E.g.:
Expand All @@ -134,15 +134,15 @@ def parse_decklist(filepath: str | Path) -> tuple[Decklist, bool, list]:
warnings: List of (entry, warning) tuples
"""
with open(filepath, encoding="utf-8") as f:
decklist, ok, warnings = parse_decklist_stream(f)
decklist, ok, warnings = parse_decklist_stream(f, cache_dir=cache_dir)

# Use file name without extension as name
decklist.name = Path(filepath).stem

return decklist, ok, warnings


def parse_decklist_stream(stream) -> tuple[Decklist, bool, list]:
def parse_decklist_stream(stream, cache_dir: Path) -> tuple[Decklist, bool, list]:
"""Parse card information from a decklist in text or MtG Arena (or mixed) format from a stream.

See:
Expand All @@ -152,7 +152,7 @@ def parse_decklist_stream(stream) -> tuple[Decklist, bool, list]:
warnings = []
ok = True
for line in stream:
m = re.search(r"([0-9]+)\s+(.+?)(?:\s+\((\S*)\)\s+(\S+))?\s*$", line)
m = re.search(r"([0-9]+)x?\s+(.+?)(?:\s+\((\S*)\)\s+(\S+))?\s*$", line)
if m:
# Extract relevant data
count = int(m.group(1))
Expand All @@ -161,15 +161,15 @@ def parse_decklist_stream(stream) -> tuple[Decklist, bool, list]:
collector_number = m.group(4) # May be None

# Validate card name
card_name, warnings_name = validate_card_name(card_name)
card_name, warnings_name = validate_card_name(card_name, cache_dir=cache_dir)
if card_name is None:
decklist.append_comment(line.rstrip())
warnings.extend([(decklist.entries[-1], level, msg) for level, msg in warnings_name])
ok = False
continue

# Validate card print
card, warnings_print = validate_print(card_name, set_id, collector_number)
card, warnings_print = validate_print(card_name, set_id, collector_number, cache_dir=cache_dir)

decklist.append_card(count, card)
warnings.extend([(decklist.entries[-1], level, msg) for level, msg in warnings_name + warnings_print])
Expand Down
Loading