|
| 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 |
0 commit comments