diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..64b2c39 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{py,rst,ini}] +indent_style = space +indent_size = 4 + +[*.{svg}] +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea1472e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2ad46b --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2020 David Fischer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..688082c --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +Magic: the Gathering Printable Set Label Generator +================================================== + +This is a small script for generating Magic: the Gathering (MTG) printable set labels +in order to organize a collection of cards. +The code is powered by the [Scryfall API][scryfall-api]. +As soon as a new set is up on Scryfall, +the label for that set can be generated and printed. + +* Print and cut out the labels +* Attach set labels to [plastic dividers][plastic-dividers] + + + +[scryfall-api]: https://scryfall.com/docs/api/sets +[plastic-dividers]: https://www.amazon.com/dp/B00S3FF1PI/ + + +## Usage + +If you're just interested in downloading and printing these set labels, +check out the [releases tab on GitHub][releases] to download and print the PDFs. + +If you want to create or customize your own labels, read on! + +The script `label-generator.py` is a small Python script to generate the printable labels. +It requires Python 3.6+ and has a few dependencies. + + + pip install -r requirements.txt # Install dependencies + python label-generator.py # Creates files in output/ + +By default, this will create one or more SVG files. +These files are vector image files that can be customized further +or printed using most modern browsers and many other tools. + +The SVGs use the free fonts [EB Garamond][garamond] bold and [Source Sans Pro][source-sans] regular. + +[releases]: https://github.com/davidfischer/mtg-printable-set-label-generator/releases +[garamond]: https://fonts.google.com/specimen/EB+Garamond +[source-sans]: https://fonts.google.com/specimen/Source+Sans+Pro + + +### Tips for printing + +The output SVGs are precisely sized for a sheet of paper (US Letter by default). +Make sure while printing in your browser or otherwise to set the margins to None. + + + +You can also "print" to a PDF. + + +### Customizing + +A lot of features can be customized by changing constants at the top of `label-generator.py`. +For example, sets can be excluded one-by-one or in groups by type or sets can be renamed. + +The labels are designed for US Letter paper but this can be customized. + +You can change how the labels are actually displayed and rendered by customizing `templates/labels.svg`. +If you change the fonts, you may also need to resize things to fit. + + +## License + +The code is available at [GitHub][home] under the [MIT license][license]. + +Some data such as set icons are unofficial Fan Content permitted under the Wizards of the Coast Fan Content Policy +and is copyright Wizards of the Coast, LLC, a subsidiary of Hasbro, Inc. +This code is not produced by, endorsed by, supported by, or affiliated with Wizards of the Coast. + +[home]: https://github.com/davidfischer/mtg-printable-set-label-generator +[license]: https://opensource.org/licenses/MIT + + +## Credits + +Special thanks goes to the users behind other printable set labels +such as those found [here][previous-set-labels]. +Using these fantastic labels definitely provided inspiration and direction +and made me want something more customizable and updatable. + +[previous-set-labels]: https://github.com/xsilium/MTG-Printable-Labels diff --git a/label-generator.py b/label-generator.py new file mode 100644 index 0000000..cb45be2 --- /dev/null +++ b/label-generator.py @@ -0,0 +1,229 @@ +import os +import subprocess +from datetime import datetime + +import jinja2 +import requests + + +BASE_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) + +ENV = jinja2.Environment( + loader=jinja2.FileSystemLoader(os.path.join(BASE_DIR, "templates")), + autoescape=jinja2.select_autoescape(["html", "xml"]), +) + +# Set types we are interested in +SET_TYPES = ( + "core", + "expansion", + "starter", # Portal, P3k, welcome decks + "masters", + "commander", + "planechase", + "draft_innovation", # Battlebond, Conspiracy + "duel_deck", # Duel Deck Elves, + "premium_deck", # Premium Deck Series: Slivers, Premium Deck Series: Graveborn + "from_the_vault", # Make sure to adjust the MINIMUM_SET_SIZE if you want these + "archenemy", + "box", + "funny", # Unglued, Unhinged, Ponies: TG, etc. + # "memorabilia", # Commander's Arsenal, Celebration Cards, World Champ Decks + # "spellbook", + # These are relatively large groups of sets + # You almost certainly don't want these + # "token", + # "promo", +) + +# Only include sets at least this size +# For reference, the smallest proper expansion is Arabian Nights with 78 cards +MINIMUM_SET_SIZE = 50 + +# Set codes you might want to ignore +IGNORED_SETS = ( + "cmb1", # Mystery Booster Playtest Cards + "amh1", # Modern Horizon Art Series +) + +# Used to rename very long set names +RENAME_SETS = { + "Fourth Edition Foreign Black Border": "Fourth Edition FBB", + "Introductory Two-Player Set": "Intro Two-Player Set", + "Commander Anthology Volume II": "Commander Anthology II", + "Planechase Anthology Planes": "Planechase Anth. Planes", + "Mystery Booster Playtest Cards": "Mystery Booster Playtest", + "World Championship Decks 1997": "World Championship 1997", + "World Championship Decks 1998": "World Championship 1998", + "World Championship Decks 1999": "World Championship 1999", + "World Championship Decks 2000": "World Championship 2000", + "World Championship Decks 2001": "World Championship 2001", + "World Championship Decks 2002": "World Championship 2002", + "World Championship Decks 2003": "World Championship 2003", + "World Championship Decks 2004": "World Championship 2004", + "Duel Decks: Elves vs. Goblins": "DD: Elves vs. Goblins", + "Duel Decks: Jace vs. Chandra": "DD: Jace vs. Chandra", + "Duel Decks: Divine vs. Demonic": "DD: Divine vs. Demonic", + "Duel Decks: Garruk vs. Liliana": "DD: Garruk vs. Liliana", + "Duel Decks: Phyrexia vs. the Coalition": "DD: Phyrexia vs. Coalition", + "Duel Decks: Elspeth vs. Tezzeret": "DD: Elspeth vs. Tezzeret", + "Duel Decks: Knights vs. Dragons": "DD: Knights vs. Dragons", + "Duel Decks: Ajani vs. Nicol Bolas": "DD: Ajani vs. Nicol Bolas", + "Duel Decks: Heroes vs. Monsters": "DD: Heroes vs. Monsters", + "Duel Decks: Speed vs. Cunning": "DD: Speed vs. Cunning", + "Duel Decks Anthology: Elves vs. Goblins": "DDA: Elves vs. Goblins", + "Duel Decks Anthology: Jace vs. Chandra": "DDA: Jace vs. Chandra", + "Duel Decks Anthology: Divine vs. Demonic": "DDA: Divine vs. Demonic", + "Duel Decks Anthology: Garruk vs. Liliana": "DDA: Garruk vs. Liliana", + "Duel Decks: Elspeth vs. Kiora": "DD: Elspeth vs. Kiora", + "Duel Decks: Zendikar vs. Eldrazi": "DD: Zendikar vs. Eldrazi", + "Duel Decks: Blessed vs. Cursed": "DD: Blessed vs. Cursed", + "Duel Decks: Nissa vs. Ob Nixilis": "DD: Nissa vs. Ob Nixilis", + "Duel Decks: Merfolk vs. Goblins": "DD: Merfolk vs. Goblins", + "Duel Decks: Elves vs. Inventors": "DD: Elves vs. Inventors", + "Premium Deck Series: Slivers": "Premium Deck Slivers", + "Premium Deck Series: Graveborn": "Premium Deck Graveborn", + "Premium Deck Series: Fire and Lightning": "Premium Deck Fire & Lightning", +} + +COLS = 4 +ROWS = 15 +WIDTH = 2790 # Width in 1/10 mm of US Letter paper +HEIGHT = 2160 +MARGIN = 200 +START_X = MARGIN +START_Y = MARGIN +DELTA_X = (WIDTH - (2 * MARGIN)) / COLS +DELTA_Y = (HEIGHT - (2 * MARGIN)) / ROWS + + +def get_set_data(): + print("Getting set data and icons from Scryfall") + + # https://scryfall.com/docs/api/sets + # https://scryfall.com/docs/api/sets/all + resp = requests.get("https://api.scryfall.com/sets") + resp.raise_for_status() + + data = resp.json()["data"] + data = [ + exp + for exp in data + if exp["set_type"] in SET_TYPES + and not exp["digital"] + and exp["code"] not in IGNORED_SETS + and exp["card_count"] >= MINIMUM_SET_SIZE + ] + data.reverse() + + return data + + +def create_set_label_data(): + """ + Create the label data for the sets + + This handles positioning of the label's (x, y) coords + """ + labels = [] + x = START_X + y = START_Y + for exp in get_set_data(): + name = RENAME_SETS.get(exp["name"], exp["name"]) + labels.append( + { + "name": name, + "code": exp["code"], + "date": datetime.strptime(exp["released_at"], "%Y-%m-%d").date(), + "icon_url": exp["icon_svg_uri"], + "x": x, + "y": y, + } + ) + + y += DELTA_Y + + # Start a new column if needed + if len(labels) % ROWS == 0: + x += DELTA_X + y = START_Y + + # Start a new page if needed + if len(labels) % (ROWS * COLS) == 0: + x = START_X + y = START_Y + + return labels + + +def create_horizontal_cutting_guides(): + """Create horizontal cutting guides to help cut the labels out straight""" + horizontal_guides = [] + for i in range(ROWS + 1): + horizontal_guides.append( + { + "x1": MARGIN / 2, + "x2": MARGIN * 0.8, + "y1": MARGIN + i * DELTA_Y, + "y2": MARGIN + i * DELTA_Y, + } + ) + horizontal_guides.append( + { + "x1": WIDTH - MARGIN / 2, + "x2": WIDTH - MARGIN * 0.8, + "y1": MARGIN + i * DELTA_Y, + "y2": MARGIN + i * DELTA_Y, + } + ) + + return horizontal_guides + + +def create_vertical_cutting_guides(): + """Create horizontal cutting guides to help cut the labels out straight""" + vertical_guides = [] + for i in range(COLS + 1): + vertical_guides.append( + { + "x1": MARGIN + i * DELTA_X, + "x2": MARGIN + i * DELTA_X, + "y1": MARGIN / 2, + "y2": MARGIN * 0.8, + } + ) + vertical_guides.append( + { + "x1": MARGIN + i * DELTA_X, + "x2": MARGIN + i * DELTA_X, + "y1": HEIGHT - MARGIN / 2, + "y2": HEIGHT - MARGIN * 0.8, + } + ) + + return vertical_guides + + +if __name__ == "__main__": + page = 1 + labels = create_set_label_data() + while labels: + exps = [] + while labels and len(exps) < (ROWS * COLS): + exps.append(labels.pop(0)) + + # Render the label template + template = ENV.get_template("labels.svg") + output = template.render( + labels=exps, + horizontal_guides=create_horizontal_cutting_guides(), + vertical_guides=create_vertical_cutting_guides(), + WIDTH=WIDTH, + HEIGHT=HEIGHT, + ) + outfile = os.path.join(BASE_DIR, "output", f"labels{page:02}.svg") + print(f"Writing {outfile}...") + with open(outfile, "w") as fd: + fd.write(output) + + page += 1 diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/readme-img/browser-printing.png b/readme-img/browser-printing.png new file mode 100644 index 0000000..3be4991 Binary files /dev/null and b/readme-img/browser-printing.png differ diff --git a/readme-img/organized-cards.jpg b/readme-img/organized-cards.jpg new file mode 100644 index 0000000..380f8b2 Binary files /dev/null and b/readme-img/organized-cards.jpg differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3b25901 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Jinja2==2.10.3 +requests==2.22.0 + +# For development only +black==19.10b0 diff --git a/templates/labels.svg b/templates/labels.svg new file mode 100644 index 0000000..196ee1e --- /dev/null +++ b/templates/labels.svg @@ -0,0 +1,29 @@ + + + {% for label in labels %} + + + {{ label.name }} + {{ label.code | upper }} - {{ label.date.strftime('%B %Y') }} + + + {% endfor %} + + + + {% for guide in horizontal_guides %} + + {% endfor %} + + + {% for guide in vertical_guides %} + + {% endfor %} + + +