Skip to content

Commit c3c7d16

Browse files
committed
First cut of the label generator
0 parents  commit c3c7d16

File tree

10 files changed

+378
-0
lines changed

10 files changed

+378
-0
lines changed

.editorconfig

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# http://editorconfig.org
2+
3+
root = true
4+
5+
[*]
6+
charset = utf-8
7+
end_of_line = lf
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
11+
[*.{py,rst,ini}]
12+
indent_style = space
13+
indent_size = 4
14+
15+
[*.{svg}]
16+
indent_style = space
17+
indent_size = 4
18+
19+
[*.md]
20+
trim_trailing_whitespace = false
21+
22+
[Makefile]
23+
indent_style = tab

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
output/

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2020 David Fischer
2+
3+
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:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
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.

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
Magic: the Gathering Printable Set Label Generator
2+
==================================================
3+
4+
This is a small script for generating Magic: the Gathering (MTG) printable set labels
5+
in order to organize a collection of cards.
6+
The code is powered by the [Scryfall API][scryfall-api].
7+
As soon as a new set is up on Scryfall,
8+
the label for that set can be generated and printed.
9+
10+
* Print and cut out the labels
11+
* Attach set labels to [plastic dividers][plastic-dividers]
12+
13+
<img src="readme-img/organized-cards.jpg">
14+
15+
[scryfall-api]: https://scryfall.com/docs/api/sets
16+
[plastic-dividers]: https://www.amazon.com/dp/B00S3FF1PI/
17+
18+
19+
## Usage
20+
21+
If you're just interested in downloading and printing these set labels,
22+
check out the [releases tab on GitHub][releases] to download and print the PDFs.
23+
24+
If you want to create or customize your own labels, read on!
25+
26+
The script `label-generator.py` is a small Python script to generate the printable labels.
27+
It requires Python 3.6+ and has a few dependencies.
28+
29+
30+
pip install -r requirements.txt # Install dependencies
31+
python label-generator.py # Creates files in output/
32+
33+
By default, this will create one or more SVG files.
34+
These files are vector image files that can be customized further
35+
or printed using most modern browsers and many other tools.
36+
37+
The SVGs use the free fonts [EB Garamond][garamond] bold and [Source Sans Pro][source-sans] regular.
38+
39+
[releases]: https://github.com/davidfischer/mtg-printable-set-label-generator/releases
40+
[garamond]: https://fonts.google.com/specimen/EB+Garamond
41+
[source-sans]: https://fonts.google.com/specimen/Source+Sans+Pro
42+
43+
44+
### Tips for printing
45+
46+
The output SVGs are precisely sized for a sheet of paper (US Letter by default).
47+
Make sure while printing in your browser or otherwise to set the margins to None.
48+
49+
<img src="readme-img/browser-printing.png">
50+
51+
You can also "print" to a PDF.
52+
53+
54+
### Customizing
55+
56+
A lot of features can be customized by changing constants at the top of `label-generator.py`.
57+
For example, sets can be excluded one-by-one or in groups by type or sets can be renamed.
58+
59+
The labels are designed for US Letter paper but this can be customized.
60+
61+
You can change how the labels are actually displayed and rendered by customizing `templates/labels.svg`.
62+
If you change the fonts, you may also need to resize things to fit.
63+
64+
65+
## License
66+
67+
The code is available at [GitHub][home] under the [MIT license][license].
68+
69+
Some data such as set icons are unofficial Fan Content permitted under the Wizards of the Coast Fan Content Policy
70+
and is copyright Wizards of the Coast, LLC, a subsidiary of Hasbro, Inc.
71+
This code is not produced by, endorsed by, supported by, or affiliated with Wizards of the Coast.
72+
73+
[home]: https://github.com/davidfischer/mtg-printable-set-label-generator
74+
[license]: https://opensource.org/licenses/MIT
75+
76+
77+
## Credits
78+
79+
Special thanks goes to the users behind other printable set labels
80+
such as those found [here][previous-set-labels].
81+
Using these fantastic labels definitely provided inspiration and direction
82+
and made me want something more customizable and updatable.
83+
84+
[previous-set-labels]: https://github.com/xsilium/MTG-Printable-Labels

label-generator.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import os
2+
import subprocess
3+
from datetime import datetime
4+
5+
import jinja2
6+
import requests
7+
8+
9+
BASE_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))
10+
11+
ENV = jinja2.Environment(
12+
loader=jinja2.FileSystemLoader(os.path.join(BASE_DIR, "templates")),
13+
autoescape=jinja2.select_autoescape(["html", "xml"]),
14+
)
15+
16+
# Set types we are interested in
17+
SET_TYPES = (
18+
"core",
19+
"expansion",
20+
"starter", # Portal, P3k, welcome decks
21+
"masters",
22+
"commander",
23+
"planechase",
24+
"draft_innovation", # Battlebond, Conspiracy
25+
"duel_deck", # Duel Deck Elves,
26+
"premium_deck", # Premium Deck Series: Slivers, Premium Deck Series: Graveborn
27+
"from_the_vault", # Make sure to adjust the MINIMUM_SET_SIZE if you want these
28+
"archenemy",
29+
"box",
30+
"funny", # Unglued, Unhinged, Ponies: TG, etc.
31+
# "memorabilia", # Commander's Arsenal, Celebration Cards, World Champ Decks
32+
# "spellbook",
33+
# These are relatively large groups of sets
34+
# You almost certainly don't want these
35+
# "token",
36+
# "promo",
37+
)
38+
39+
# Only include sets at least this size
40+
# For reference, the smallest proper expansion is Arabian Nights with 78 cards
41+
MINIMUM_SET_SIZE = 50
42+
43+
# Set codes you might want to ignore
44+
IGNORED_SETS = (
45+
"cmb1", # Mystery Booster Playtest Cards
46+
"amh1", # Modern Horizon Art Series
47+
)
48+
49+
# Used to rename very long set names
50+
RENAME_SETS = {
51+
"Fourth Edition Foreign Black Border": "Fourth Edition FBB",
52+
"Introductory Two-Player Set": "Intro Two-Player Set",
53+
"Commander Anthology Volume II": "Commander Anthology II",
54+
"Planechase Anthology Planes": "Planechase Anth. Planes",
55+
"Mystery Booster Playtest Cards": "Mystery Booster Playtest",
56+
"World Championship Decks 1997": "World Championship 1997",
57+
"World Championship Decks 1998": "World Championship 1998",
58+
"World Championship Decks 1999": "World Championship 1999",
59+
"World Championship Decks 2000": "World Championship 2000",
60+
"World Championship Decks 2001": "World Championship 2001",
61+
"World Championship Decks 2002": "World Championship 2002",
62+
"World Championship Decks 2003": "World Championship 2003",
63+
"World Championship Decks 2004": "World Championship 2004",
64+
"Duel Decks: Elves vs. Goblins": "DD: Elves vs. Goblins",
65+
"Duel Decks: Jace vs. Chandra": "DD: Jace vs. Chandra",
66+
"Duel Decks: Divine vs. Demonic": "DD: Divine vs. Demonic",
67+
"Duel Decks: Garruk vs. Liliana": "DD: Garruk vs. Liliana",
68+
"Duel Decks: Phyrexia vs. the Coalition": "DD: Phyrexia vs. Coalition",
69+
"Duel Decks: Elspeth vs. Tezzeret": "DD: Elspeth vs. Tezzeret",
70+
"Duel Decks: Knights vs. Dragons": "DD: Knights vs. Dragons",
71+
"Duel Decks: Ajani vs. Nicol Bolas": "DD: Ajani vs. Nicol Bolas",
72+
"Duel Decks: Heroes vs. Monsters": "DD: Heroes vs. Monsters",
73+
"Duel Decks: Speed vs. Cunning": "DD: Speed vs. Cunning",
74+
"Duel Decks Anthology: Elves vs. Goblins": "DDA: Elves vs. Goblins",
75+
"Duel Decks Anthology: Jace vs. Chandra": "DDA: Jace vs. Chandra",
76+
"Duel Decks Anthology: Divine vs. Demonic": "DDA: Divine vs. Demonic",
77+
"Duel Decks Anthology: Garruk vs. Liliana": "DDA: Garruk vs. Liliana",
78+
"Duel Decks: Elspeth vs. Kiora": "DD: Elspeth vs. Kiora",
79+
"Duel Decks: Zendikar vs. Eldrazi": "DD: Zendikar vs. Eldrazi",
80+
"Duel Decks: Blessed vs. Cursed": "DD: Blessed vs. Cursed",
81+
"Duel Decks: Nissa vs. Ob Nixilis": "DD: Nissa vs. Ob Nixilis",
82+
"Duel Decks: Merfolk vs. Goblins": "DD: Merfolk vs. Goblins",
83+
"Duel Decks: Elves vs. Inventors": "DD: Elves vs. Inventors",
84+
"Premium Deck Series: Slivers": "Premium Deck Slivers",
85+
"Premium Deck Series: Graveborn": "Premium Deck Graveborn",
86+
"Premium Deck Series: Fire and Lightning": "Premium Deck Fire & Lightning",
87+
}
88+
89+
COLS = 4
90+
ROWS = 15
91+
WIDTH = 2790 # Width in 1/10 mm of US Letter paper
92+
HEIGHT = 2160
93+
MARGIN = 200
94+
START_X = MARGIN
95+
START_Y = MARGIN
96+
DELTA_X = (WIDTH - (2 * MARGIN)) / COLS
97+
DELTA_Y = (HEIGHT - (2 * MARGIN)) / ROWS
98+
99+
100+
def get_set_data():
101+
print("Getting set data and icons from Scryfall")
102+
103+
# https://scryfall.com/docs/api/sets
104+
# https://scryfall.com/docs/api/sets/all
105+
resp = requests.get("https://api.scryfall.com/sets")
106+
resp.raise_for_status()
107+
108+
data = resp.json()["data"]
109+
data = [
110+
exp
111+
for exp in data
112+
if exp["set_type"] in SET_TYPES
113+
and not exp["digital"]
114+
and exp["code"] not in IGNORED_SETS
115+
and exp["card_count"] >= MINIMUM_SET_SIZE
116+
]
117+
data.reverse()
118+
119+
return data
120+
121+
122+
def create_set_label_data():
123+
"""
124+
Create the label data for the sets
125+
126+
This handles positioning of the label's (x, y) coords
127+
"""
128+
labels = []
129+
x = START_X
130+
y = START_Y
131+
for exp in get_set_data():
132+
name = RENAME_SETS.get(exp["name"], exp["name"])
133+
labels.append(
134+
{
135+
"name": name,
136+
"code": exp["code"],
137+
"date": datetime.strptime(exp["released_at"], "%Y-%m-%d").date(),
138+
"icon_url": exp["icon_svg_uri"],
139+
"x": x,
140+
"y": y,
141+
}
142+
)
143+
144+
y += DELTA_Y
145+
146+
# Start a new column if needed
147+
if len(labels) % ROWS == 0:
148+
x += DELTA_X
149+
y = START_Y
150+
151+
# Start a new page if needed
152+
if len(labels) % (ROWS * COLS) == 0:
153+
x = START_X
154+
y = START_Y
155+
156+
return labels
157+
158+
159+
def create_horizontal_cutting_guides():
160+
"""Create horizontal cutting guides to help cut the labels out straight"""
161+
horizontal_guides = []
162+
for i in range(ROWS + 1):
163+
horizontal_guides.append(
164+
{
165+
"x1": MARGIN / 2,
166+
"x2": MARGIN * 0.8,
167+
"y1": MARGIN + i * DELTA_Y,
168+
"y2": MARGIN + i * DELTA_Y,
169+
}
170+
)
171+
horizontal_guides.append(
172+
{
173+
"x1": WIDTH - MARGIN / 2,
174+
"x2": WIDTH - MARGIN * 0.8,
175+
"y1": MARGIN + i * DELTA_Y,
176+
"y2": MARGIN + i * DELTA_Y,
177+
}
178+
)
179+
180+
return horizontal_guides
181+
182+
183+
def create_vertical_cutting_guides():
184+
"""Create horizontal cutting guides to help cut the labels out straight"""
185+
vertical_guides = []
186+
for i in range(COLS + 1):
187+
vertical_guides.append(
188+
{
189+
"x1": MARGIN + i * DELTA_X,
190+
"x2": MARGIN + i * DELTA_X,
191+
"y1": MARGIN / 2,
192+
"y2": MARGIN * 0.8,
193+
}
194+
)
195+
vertical_guides.append(
196+
{
197+
"x1": MARGIN + i * DELTA_X,
198+
"x2": MARGIN + i * DELTA_X,
199+
"y1": HEIGHT - MARGIN / 2,
200+
"y2": HEIGHT - MARGIN * 0.8,
201+
}
202+
)
203+
204+
return vertical_guides
205+
206+
207+
if __name__ == "__main__":
208+
page = 1
209+
labels = create_set_label_data()
210+
while labels:
211+
exps = []
212+
while labels and len(exps) < (ROWS * COLS):
213+
exps.append(labels.pop(0))
214+
215+
# Render the label template
216+
template = ENV.get_template("labels.svg")
217+
output = template.render(
218+
labels=exps,
219+
horizontal_guides=create_horizontal_cutting_guides(),
220+
vertical_guides=create_vertical_cutting_guides(),
221+
WIDTH=WIDTH,
222+
HEIGHT=HEIGHT,
223+
)
224+
outfile = os.path.join(BASE_DIR, "output", f"labels{page:02}.svg")
225+
print(f"Writing {outfile}...")
226+
with open(outfile, "w") as fd:
227+
fd.write(output)
228+
229+
page += 1

output/.gitkeep

Whitespace-only changes.

readme-img/browser-printing.png

171 KB
Loading

readme-img/organized-cards.jpg

116 KB
Loading

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Jinja2==2.10.3
2+
requests==2.22.0
3+
4+
# For development only
5+
black==19.10b0

templates/labels.svg

Lines changed: 29 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)