2018-07-30 15:12:05 -07:00

1304 lines
53 KiB
Python

from __future__ import print_function, absolute_import
import os
import codecs
import json
import sys
import argparse
import copy
import fnmatch
import pkg_resources
import unicodedata
from collections import Counter, defaultdict
import reportlab.lib.pagesizes as pagesizes
from reportlab.lib.units import cm
from .cards import Card
from .cards import CardType
from .draw import DividerDrawer
LOCATION_CHOICES = ["tab", "body-top", "hide"]
NAME_ALIGN_CHOICES = ["left", "right", "centre", "edge"]
TAB_SIDE_CHOICES = ["left", "right", "left-alternate", "right-alternate",
"centre", "full"]
TEXT_CHOICES = ["card", "rules", "blank"]
EDITION_CHOICES = ["1", "2", "latest", "all"]
EXPANSION_CHOICES = ["adventures", "alchemy", "base", "cornucopia", "dark ages",
"dominion1stEdition", "dominion2ndEdition", "dominion2ndEditionUpgrade",
"empires", "guilds", "hinterlands",
"intrigue1stEdition", "intrigue2ndEdition", "intrigue2ndEditionUpgrade",
"promo", "prosperity", "seaside", "nocturne"]
FAN_CHOICES = ["animals"]
ORDER_CHOICES = ["expansion", "global", "colour", "cost"]
LANGUAGE_DEFAULT = 'en_us' # the primary language used if a language's parts are missing
LANGUAGE_XX = 'xx' # a dummy language for starting translations
def get_languages(path):
languages = []
for name in pkg_resources.resource_listdir('domdiv', path):
dir_path = os.path.join(path, name)
if pkg_resources.resource_isdir('domdiv', dir_path):
cards_file = os.path.join(dir_path, "cards_{}.json".format(name))
sets_file = os.path.join(dir_path, "sets_{}.json".format(name))
types_file = os.path.join(dir_path, "types_{}.json".format(name))
if (pkg_resources.resource_exists('domdiv', cards_file) and
pkg_resources.resource_exists('domdiv', sets_file) and
pkg_resources.resource_exists('domdiv', types_file)):
languages.append(name)
if LANGUAGE_XX in languages:
languages.remove(LANGUAGE_XX)
return languages
LANGUAGE_CHOICES = get_languages("card_db")
def add_opt(options, option, value):
assert not hasattr(options, option)
setattr(options, option, value)
def parse_opts(cmdline_args=None):
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Generate Dominion Dividers",
epilog="Source can be found at 'https://github.com/sumpfork/dominiontabs'. "
"An online version can be found at 'http://domtabs.sandflea.org/'. ")
# Basic Divider Information
group_basic = parser.add_argument_group(
'Basic Divider Options',
'Basic choices for the dividers.')
group_basic.add_argument(
'--outfile', '-o',
dest="outfile",
default="dominion_dividers.pdf",
help="The output file name.")
group_basic.add_argument(
"--papersize",
dest="papersize",
default=None,
help="The size of paper to use; '<%%f>x<%%f>' (size in cm), or 'A4', or 'LETTER'. "
"If not specified, it will default to system defaults, and if the system defaults "
"are not found, then to 'LETTER'.")
group_basic.add_argument(
"--language", "-l",
dest="language",
default=LANGUAGE_DEFAULT,
choices=LANGUAGE_CHOICES,
help="Language of divider text.")
group_basic.add_argument(
"--orientation",
choices=["horizontal", "vertical"],
dest="orientation",
default="horizontal",
help="Either horizontal or vertical divider orientation.")
group_basic.add_argument(
"--size",
dest="size",
default='normal',
help="Dimentions of the cards to use with the dividers '<%%f>x<%%f>' (size in cm), "
"or 'normal' = '9.1x5.9', or 'sleeved' = '9.4x6.15'.")
group_basic.add_argument(
"--sleeved",
action="store_true",
dest="sleeved",
help="Same as --size=sleeved.")
group_basic.add_argument(
"--order",
choices=ORDER_CHOICES,
default="expansion",
dest="order",
help="Sort order for the dividers: "
" 'global' will sort by card name;"
" 'expansion' will sort by expansion, then card name;"
" 'colour' will sort by card type, then card name;"
" 'cost' will sort by expansion, then card cost, then name.")
# Divider Body
group_body = parser.add_argument_group(
'Divider Body',
'Changes what is displayed on the body of the dividers.')
group_body.add_argument(
"--front",
choices=TEXT_CHOICES,
dest="text_front",
default="card",
help="Text to print on the front of the divider; "
"'card' will print the text from the game card; "
"'rules' will print additional rules for the game card; "
"'blank' will not print text on the divider.")
group_body.add_argument(
"--back",
choices=TEXT_CHOICES + ["none"],
dest="text_back",
default="rules",
help="Text to print on the back of the divider; "
"'card' will print the text from the game card; "
"'rules' will print additional rules for the game card; "
"'blank' will not print text on the divider; "
"'none' will prevent the back pages from printing. ")
group_body.add_argument(
"--count",
action="store_true",
dest="count",
help="Display the card count on the body of card dividers "
"and the randomizer count on the body of expansion dividers.")
group_body.add_argument(
"--types",
action="store_true",
dest="types",
help="Display card type on the body of the divider.")
# Divider Tab
group_tab = parser.add_argument_group(
'Divider Tab',
'Changes what is displayed on on the Divider Tab.')
group_tab.add_argument(
"--tab-side",
choices=TAB_SIDE_CHOICES,
dest="tab_side",
default="right-alternate",
help="Alignment of tab; "
"'left'/'right' forces all tabs to left/right side; "
"'left-alternate' will start on the left and then toggle between left and right for the tabs; "
"'right-alternate' will start on the right and then toggle between right and left for the tabs; "
"'centre' will force all label tabs to the centre; "
"'full' will force all label tabs to be full width of the divider.")
group_tab.add_argument(
"--tab-name-align",
choices=NAME_ALIGN_CHOICES + ["center"],
dest="tab_name_align",
default="left",
help="Alignment of text on the tab; "
"The 'edge' option will align the card name to the outside edge of the "
"tab, so that when using tabs on alternating sides, "
"the name is less likely to be hidden by the tab in front "
"(edge will revert to left when tab_side is full since there is no edge in that case).")
group_tab.add_argument(
"--tabwidth",
type=float,
default=4.0,
help="Width in cm of stick-up tab (ignored if --tab_side is 'full' or --tabs_only is used).")
group_tab.add_argument(
"--cost",
action="append",
choices=LOCATION_CHOICES,
default=['tab'],
help="Where to display the card cost; may be set to "
"'hide' to indicate it should not be displayed, or "
"given multiple times to show it in multiple places.")
group_tab.add_argument(
"--set-icon",
action="append",
choices=LOCATION_CHOICES,
default=['tab'],
help="Where to display the set icon; may be set to "
"'hide' to indicate it should not be displayed, or "
"given multiple times to show it in multiple places.")
group_tab.add_argument(
"--no-tab-artwork",
action="store_true",
dest="no_tab_artwork",
help="Don't show background artwork on tabs.")
group_tab.add_argument(
"--use-text-set-icon",
action="store_true",
dest="use_text_set_icon",
help="Use text/letters to represent a card's set instead of the set icon.")
# Expanion Dividers
group_expansion = parser.add_argument_group(
'Expansion Dividers',
'Adding separator dividers for each expansion.')
group_expansion.add_argument(
"--expansion-dividers",
action="store_true",
dest="expansion_dividers",
help="Add dividers describing each expansion set. "
"A list of cards in the expansion will be shown on the front of the divider.")
group_expansion.add_argument(
"--centre-expansion-dividers",
action="store_true",
dest="centre_expansion_dividers",
help='Centre the tabs on expansion dividers.')
group_expansion.add_argument(
"--expansion-dividers-long-name",
action="store_true",
dest="expansion_dividers_long_name",
help="Use the long name with edition information on the expansion divider tab. "
"Without this, the shorter expansion name is used on the expansion divider tab.")
# Divider Selection
group_select = parser.add_argument_group(
'Divider Selection',
'What expansions are used, and grouping of dividers.')
group_select.add_argument(
"--expansions", "--expansion",
nargs="*",
action="append",
dest="expansions",
help="Limit dividers to only the specified expansions. "
"If no limits are set, then all expansions are included. "
"Expansion names can also be given in the language specified by "
"the --language parameter. Any expansion with a space in the name must "
"be enclosed in double quotes. This may be called multiple times. "
"Values are not case sensitive. Wildcards may be used: "
"'*' any number of characters, '?' matches any single character, "
"'[seq]' matches any character in seq, and '[!seq]' matches any character not in seq. "
"For example, 'dominion*' will match all expansions that start with 'dominion'. "
"Choices available in all languages include: %s" %
", ".join("%s" % x for x in EXPANSION_CHOICES))
group_select.add_argument(
"--fan",
nargs="*",
action="append",
dest="fan",
help="Add dividers from the specified fan made expansions. "
"If this option is not used, no fan expansions will be included. "
"Fan made expansion names can also be given in the language specified by "
"the --language parameter. Any fan expansion with a space in the name must "
"be enclosed in double quotes. This may be called multiple times. "
"Values are not case sensitive. Wildcards may be used: "
"'*' any number of characters, '?' matches any single character, "
"'[seq]' matches any character in seq, and '[!seq]' matches any character not in seq. "
"Choices available in all languages include: %s" %
", ".join("%s" % x for x in FAN_CHOICES))
group_select.add_argument(
"--edition",
choices=EDITION_CHOICES,
dest="edition",
default="all",
help="Editions to include: "
"'1' is for all 1st Editions; "
"'2' is for all 2nd Editions; "
"'latest' is for the latest edition for each expansion; "
"'all' is for all editions of expansions; "
" This can be combined with other options to refine the expansions to include in the output.")
group_select.add_argument(
"--upgrade-with-expansion",
action="store_true",
dest="upgrade_with_expansion",
help="Include any new edition upgrade cards with the expansion being upgraded.")
group_select.add_argument(
"--base-cards-with-expansion",
action="store_true",
help="Print the base cards as part of the expansion (i.e., a divider for 'Silver' "
"will be printed as both a 'Dominion' card and as an 'Intrigue 1st Edition' card). "
"If this option is not given, all base cards are placed in their own 'Base' expansion.")
group_select.add_argument(
"--special-card-groups",
action="store_true",
help="Group cards that generally are used together "
"(e.g., Shelters, Tournament and Prizes, Urchin/Mercenary, etc.).")
group_select.add_argument(
"--no-trash",
action="store_true",
dest="no_trash",
help="Exclude Trash from cards.")
group_select.add_argument(
"--curse10",
action="store_true",
dest="curse10",
help="Package Curse cards into groups of ten cards.")
group_select.add_argument(
"--start-decks",
action="store_true",
dest="start_decks",
help="Include four start decks with the Base cards.")
group_select.add_argument(
"--include-blanks",
action="store_true",
help="Include a few dividers with extra text.")
group_select.add_argument(
"--exclude-events",
action="store_true",
help="Group all 'Event' cards across all expansions into one divider.")
group_select.add_argument(
"--exclude-landmarks",
action="store_true",
help="Group all 'Landmark' cards across all expansions into one divider.")
# Divider Sleeves/Wrappers
group_wrapper = parser.add_argument_group(
'Card Sleeves/Wrappers',
'Generating dividers that are card sleeves/wrappers.')
group_wrapper.add_argument(
"--wrapper",
action="store_true",
dest="wrapper",
help="Draw sleeves (aka wrapper) for the cards instead of a divider for the cards.")
group_wrapper.add_argument(
"--thickness",
type=float,
default=2.0,
help="Thickness of a stack of 60 cards (Copper) in centimeters. "
"Typically unsleeved cards are 2.0, thin sleeved cards are 2.4, and thick sleeved cards are 3.2. "
"This is only valid with the --wrapper option.")
group_wrapper.add_argument(
"--sleeved-thick",
action="store_true",
dest="sleeved_thick",
help="Same as --size=sleeved --thickness 3.2.")
group_wrapper.add_argument(
"--sleeved-thin",
action="store_true",
dest="sleeved_thin",
help="Same as --size=sleeved --thickness 2.4.")
group_wrapper.add_argument(
"--notch-length",
type=float,
default=0.0,
help="Length of thumb notch on wrapper in centimeters "
"(a value of 0.0 means no notch on wrapper). "
"This can make it easier to remove the actual cards from the wrapper. "
"This is only valid with the --wrapper option.")
group_wrapper.add_argument(
"--notch",
action="store_true",
dest="notch",
help="Same as --notch_length thickness 1.5.")
# Printing
group_printing = parser.add_argument_group(
'Printing',
'Changes how the Dividers are printed.')
group_printing.add_argument(
"--minmargin",
dest="minmargin",
default="1x1",
help="Page margin in cm in the form '<%%f>x<%%f>', left/right x top/bottom).")
group_printing.add_argument(
"--cropmarks",
action="store_true",
dest="cropmarks",
help="Print crop marks on both sides, rather than tab outlines on the front side.")
group_printing.add_argument(
"--linewidth",
type=float,
default=0.1,
help="Width of lines for card outlines and crop marks.")
group_printing.add_argument(
"--back-offset",
type=float,
dest="back_offset",
default=0,
help="Back page horizontal offset points to shift to the right. Only needed for some printers.")
group_printing.add_argument(
"--back-offset-height",
type=float,
dest="back_offset_height",
default=0,
help="Back page vertical offset points to shift upward. Only needed for some printers.")
group_printing.add_argument(
"--vertical-gap",
type=float,
default=0.0,
help="Vertical gap between dividers in centimeters.")
group_printing.add_argument(
"--horizontal-gap",
type=float,
default=0.0,
help="Horizontal gap between dividers in centimeters.")
group_printing.add_argument(
"--no-page-footer",
action="store_true",
dest="no_page_footer",
help="Do not print the expansion name at the bottom of the page.")
group_printing.add_argument(
"--num-pages",
type=int,
default=-1,
help="Stop generating dividers after this many pages, -1 for all.")
group_printing.add_argument(
"--tabs-only",
action="store_true",
dest="tabs_only",
help="Draw only the divider tabs and no divider outlines. "
"Used to print the divider tabs on labels.")
group_printing.add_argument(
"--info",
action="store_true",
dest="info",
help="Add a page that has all the options used for the file.")
group_printing.add_argument(
"--info-all",
action="store_true",
dest="info_all",
help="Same as --info, but includes pages with all the possible options that can be used.")
group_printing.add_argument(
"--preview",
action='store_true',
help="Only generate a preview png image of the first page"
)
group_printing.add_argument(
"--preview-resolution",
type=int,
default=150,
help="resolution in DPI to render preview at, for --preview option")
# Special processing
group_special = parser.add_argument_group(
'Miscellaneous',
'These options are generally not used.')
group_special.add_argument(
"--cardlist",
dest="cardlist",
help="Path to file that enumerates each card to be printed on its own line.")
group_special.add_argument(
"--write-json",
action="store_true",
dest="write_json",
help="Write json version of card definitions and extras.")
options = parser.parse_args(args=cmdline_args)
# Need to do these while we have access to the parser
options.argv = sys.argv if options.info or options.info_all else None
options.help = parser.format_help() if options.info_all else None
return options
def clean_opts(options):
if options.sleeved_thick:
options.thickness = 3.2
options.sleeved = True
if options.sleeved_thin:
options.thickness = 2.4
options.sleeved = True
if options.notch:
options.notch_length = 1.5
if options.expansions is None:
# No instance given, so default to all Official expansions
options.expansions = ['*']
else:
# options.expansions is a list of lists. Reduce to single lowercase list
options.expansions = [item.lower() for sublist in options.expansions for item in sublist]
if 'none' in options.expansions:
# keyword to indicate no options. Same as --expansions without any expansions given.
options.expansions = []
if options.fan is None:
# No instance given, so default to no Fan expansions
options.fan = []
else:
# options.fan is a list of lists. Reduce to single lowercase list
options.fan = [item.lower() for sublist in options.fan for item in sublist]
if 'none' in options.fan:
# keyword to indicate no options. Same as --fan without any expansions given
options.fan = []
return options
def parseDimensions(dimensionsStr):
x, y = dimensionsStr.upper().split('X', 1)
return (float(x) * cm, float(y) * cm)
def generate_sample(options):
import cStringIO
from wand.image import Image
buf = cStringIO.StringIO()
options.num_pages = 1
options.outfile = buf
generate(options)
sample_out = cStringIO.StringIO()
with Image(blob=buf.getvalue(), resolution=options.preview_resolution) as sample:
sample.format = 'png'
sample.save(sample_out)
return sample_out
def parse_papersize(spec):
papersize = None
if not spec:
if os.path.exists("/etc/papersize"):
papersize = open("/etc/papersize").readline().upper()
else:
papersize = 'LETTER'
else:
papersize = spec.upper()
try:
paperwidth, paperheight = getattr(pagesizes, papersize)
except AttributeError:
try:
paperwidth, paperheight = parseDimensions(papersize)
print(('Using custom paper size, %.2fcm x %.2fcm'.format(
paperwidth / cm, paperheight / cm)))
except ValueError:
paperwidth, paperheight = pagesizes.LETTER
return paperwidth, paperheight
def parse_cardsize(spec, sleeved):
spec = spec.upper()
if spec == 'SLEEVED' or sleeved:
dominionCardWidth, dominionCardHeight = (9.4 * cm, 6.15 * cm)
print(('Using sleeved card size, {:.2f}cm x {:.2f}cm'.format(
dominionCardWidth / cm, dominionCardHeight / cm)))
elif spec in ['NORMAL', 'UNSLEEVED']:
dominionCardWidth, dominionCardHeight = (9.1 * cm, 5.9 * cm)
print(('Using normal card size, {:.2f}cm x{:.2f}cm'.format(
dominionCardWidth / cm, dominionCardHeight / cm)))
else:
dominionCardWidth, dominionCardHeight = parseDimensions(spec)
print(('Using custom card size, {.2f}cm x {.2f}cm'.format(
dominionCardWidth / cm, dominionCardHeight / cm)))
return dominionCardWidth, dominionCardHeight
def get_resource_stream(path):
return codecs.EncodedFile(pkg_resources.resource_stream('domdiv', path), "utf-8")
def find_index_of_object(lst=[], attributes={}):
# Returns the index of the first object in lst that matches the given attributes. Otherwise returns None.
# attributes is a dict of key: value pairs. Object attributes that are lists are checked to have value in them.
for i, d in enumerate(lst):
# Set match to false just in case there are no attributes.
match = False
for key, value in attributes.items():
# if anything does not match, then break out and start the next one.
match = hasattr(d, key)
if match:
test = getattr(d, key, None)
if type(test) is list:
match = value in test
else:
match = value == test
if not match:
break
if match:
# If all the attributes are found, then we have a match
return i
# nothing matched
return None
def read_card_data(options):
# Read in the card types
types_db_filepath = os.path.join("card_db", "types_db.json")
with get_resource_stream(types_db_filepath) as typefile:
Card.types = json.loads(typefile.read().decode('utf-8'), object_hook=CardType.decode_json)
assert Card.types, "Could not load any card types from database"
# extract unique types
type_list = []
for c in Card.types:
type_list = list(set(c.getTypeNames()) | set(type_list))
# set up the basic type translation. The actual language will be added later.
Card.type_names = {}
for t in type_list:
Card.type_names[t] = t
# turn Card.types into a dictionary for later
Card.types = dict(((c.getTypeNames(), c) for c in Card.types))
# Read in the card database
card_db_filepath = os.path.join("card_db", "cards_db.json")
with get_resource_stream(card_db_filepath) as cardfile:
cards = json.loads(cardfile.read().decode('utf-8'), object_hook=Card.decode_json)
assert cards, "Could not load any cards from database"
set_db_filepath = os.path.join("card_db", "sets_db.json")
with get_resource_stream(set_db_filepath) as setfile:
Card.sets = json.loads(setfile.read().decode('utf-8'))
assert Card.sets, "Could not load any sets from database"
for s in Card.sets:
# Make sure these are set either True or False
Card.sets[s]['no_randomizer'] = Card.sets[s].get('no_randomizer', False)
Card.sets[s]['fan'] = Card.sets[s].get('fan', False)
# Remove the Trash card. Do early before propagating to various sets.
if options.no_trash:
i = find_index_of_object(cards, {'card_tag': 'Trash'})
if i is not None:
del cards[i]
# Repackage Curse cards into 10 per divider. Do early before propagating to various sets.
if options.curse10:
i = find_index_of_object(cards, {'card_tag': 'Curse'})
if i is not None:
new_cards = []
cards_remaining = cards[i].getCardCount()
while cards_remaining > 10:
# make a new copy of the card and set count to 10
new_card = copy.deepcopy(cards[i])
new_card.setCardCount(10)
new_cards.append(new_card)
cards_remaining -= 10
# Adjust original Curse card to the remaining cards (should be 10)
cards[i].setCardCount(cards_remaining)
# Add the new dividers
cards.extend(new_cards)
# Create Start Deck dividers. 4 sets. Adjust totals for other cards, too.
# Do early before propagating to various sets.
# The card database contains one prototype divider that needs to be either duplicated or deleted.
if options.start_decks:
# Find the index to the individual cards that need changed in the cards list
StartDeck_index = find_index_of_object(cards, {'card_tag': 'Start Deck'})
Copper_index = find_index_of_object(cards, {'card_tag': 'Copper'})
Estate_index = find_index_of_object(cards, {'card_tag': 'Estate'})
if Copper_index is None or Estate_index is None or StartDeck_index is None:
# Something is wrong, can't find one or more of the cards that need to change
print("Error - cannot create Start Decks")
# Remove the Start Deck prototype if we can
if StartDeck_index is not None:
del cards[StartDeck_index]
else:
# Start Deck Constants
STARTDECK_COPPERS = 7
STARTDECK_ESTATES = 3
STARTDECK_NUMBER = 4
# Add correct card counts to Start Deck prototype. This will be used to make copies.
cards[StartDeck_index].setCardCount(STARTDECK_COPPERS)
cards[StartDeck_index].addCardCount([int(STARTDECK_ESTATES)])
# Make new Start Deck Dividers and adjust the corresponding card counts
for x in range(0, STARTDECK_NUMBER):
# Add extra copies of the Start Deck prototype.
# But don't need to add the first one again, since the prototype is already there.
if x > 0:
cards.append(copy.deepcopy(cards[StartDeck_index]))
# Note: By appending, it should not change any of the index values being used
# Remove Copper and Estate card counts from their dividers
cards[Copper_index].setCardCount(cards[Copper_index].getCardCount() - STARTDECK_COPPERS)
cards[Estate_index].setCardCount(cards[Estate_index].getCardCount() - STARTDECK_ESTATES)
else:
# Remove Start Deck prototype. It is not needed.
StartDeck_index = find_index_of_object(cards, {'card_tag': 'Start Deck'})
if StartDeck_index is not None:
del cards[StartDeck_index]
# Set cardset_tag and expand cards that are used in multiple sets
new_cards = []
for card in cards:
sets = list(card.cardset_tags)
if len(sets) > 0:
# Set and save the first one
card.cardset_tag = sets.pop(0)
new_cards.append(card)
for s in sets:
# for the rest, create a copy of the first
if s:
new_card = copy.deepcopy(card)
new_card.cardset_tag = s
new_cards.append(new_card)
cards = new_cards
# Make sure each card has the right image file.
for card in cards:
card.image = card.setImage()
return cards
class CardSorter(object):
def __init__(self, order, baseCards):
self.order = order
if order == "global":
self.sort_key = self.by_global_sort_key
elif order == "colour":
self.sort_key = self.by_colour_sort_key
elif order == "cost":
self.sort_key = self.by_cost_sort_key
else:
self.sort_key = self.by_expansion_sort_key
self.baseOrder = ['Copper', 'Silver', 'Gold', 'Platinum', 'Potion',
'Curse', 'Estate', 'Duchy', 'Province', 'Colony',
'Trash', 'Start Deck']
self.baseCards = []
for tag in self.baseOrder:
if tag in baseCards:
self.baseCards.append(baseCards[tag])
del baseCards[tag]
# now pick up those that have not been specified
for tag in baseCards:
self.baseCards.append(baseCards[tag])
# When sorting cards, want to always put "base" cards after all
# kingdom cards, and order the base cards in a particular order
# (ie, all normal treasures by worth, then potion, then all
# normal VP cards by worth, then Trash)
def baseIndex(self, name):
try:
return self.baseCards.index(name)
except Exception:
return -1
def isBaseExpansionCard(self, card):
return card.cardset_tag.lower() != 'base' and card.name in self.baseCards
def by_global_sort_key(self, card):
return int(card.isExpansion()), self.baseIndex(card.name), self.strip_accents(card.name)
def by_expansion_sort_key(self, card):
return card.cardset, int(card.isExpansion()), self.baseIndex(
card.name), self.strip_accents(card.name)
def by_colour_sort_key(self, card):
return card.getType().getTypeNames(), self.strip_accents(card.name)
def by_cost_sort_key(self, card):
return card.cardset, int(card.isExpansion()), card.get_total_cost(card), self.strip_accents(card.name)
@staticmethod
def strip_accents(s):
return ''.join(c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn')
def __call__(self, card):
return self.sort_key(card)
def add_card_text(options, cards, language='en_us'):
language = language.lower()
# Read in the card text file
card_text_filepath = os.path.join("card_db",
language,
"cards_" + language.lower() + ".json")
with get_resource_stream(card_text_filepath) as card_text_file:
card_text = json.loads(card_text_file.read().decode('utf-8'))
assert language, "Could not load card text for %r" % language
# Now apply to all the cards
for card in cards:
if card.card_tag in card_text:
if 'name' in card_text[card.card_tag].keys():
card.name = card_text[card.card_tag]['name']
if 'description' in card_text[card.card_tag].keys():
card.description = card_text[card.card_tag]['description']
if 'extra' in card_text[card.card_tag].keys():
card.extra = card_text[card.card_tag]['extra']
return cards
def add_set_text(options, sets, language='en_us'):
language = language.lower()
# Read in the set text and store for later
set_text_filepath = os.path.join("card_db",
language,
"sets_{}.json".format(language))
with get_resource_stream(set_text_filepath) as set_text_file:
set_text = json.loads(set_text_file.read().decode('utf-8'))
assert set_text, "Could not load set text for %r" % language
# Now apply to all the sets
for s in sets:
if s in set_text:
for key in set_text[s]:
sets[s][key] = set_text[s][key]
return sets
def add_type_text(options, types={}, language='en_us'):
language = language.lower()
# Read in the type text and store for later
type_text_filepath = os.path.join("card_db",
language,
"types_{}.json".format(language))
with get_resource_stream(type_text_filepath) as type_text_file:
type_text = json.loads(type_text_file.read().decode('utf-8'))
assert type_text, "Could not load type text for %r" % language
# Now apply to all the types
used = {}
for type in types:
if type in type_text:
types[type] = type_text[type]
used[type] = True
for type in type_text:
if type not in used:
types[type] = type_text[type]
return types
def add_bonus_regex(options, language='en_us'):
language = language.lower()
# Read in the bonus regex terms
bonus_regex_filepath = os.path.join("card_db",
language,
"bonuses_{}.json".format(language))
with get_resource_stream(bonus_regex_filepath) as bonus_regex_file:
bonus_regex = json.loads(bonus_regex_file.read().decode('utf-8'))
assert bonus_regex, "Could not load bonus keywords for %r" % language
if not bonus_regex:
bonus_regex = {}
return bonus_regex
def combine_cards(cards, old_card_type, new_card_tag, new_cardset_tag, new_type):
holder = Card(name='*Replace Later*',
card_tag=new_card_tag,
group_tag=new_card_tag,
cardset_tag=new_cardset_tag,
types=(new_type, ),
count=0)
holder.image = holder.setImage()
filteredCards = []
for c in cards:
if c.isType(old_card_type):
holder.addCardCount(c.count) # keep track of count and skip card
else:
filteredCards.append(c) # Not the right type, keep card
if holder.getCardCount() > 0:
filteredCards.append(holder)
return filteredCards
def filter_sort_cards(cards, options):
# Filter out cards by edition
if options.edition and options.edition != "all":
keep_sets = []
for set_tag in Card.sets:
for edition in Card.sets[set_tag]["edition"]:
if options.edition == edition:
keep_sets.append(set_tag)
keep_cards = [] # holds the cards that are to be kept
for card in cards:
if card.cardset_tag in keep_sets:
keep_cards.append(card)
cards = keep_cards
# Combine upgrade cards with their expansion
if options.upgrade_with_expansion:
for card in cards:
if card.cardset_tag == 'dominion2ndEditionUpgrade':
card.cardset_tag = 'dominion1stEdition'
elif card.cardset_tag == 'intrigue2ndEditionUpgrade':
card.cardset_tag = 'intrigue1stEdition'
# Combine all Events across all expansions
if options.exclude_events:
cards = combine_cards(cards,
old_card_type="Event",
new_type="Events",
new_card_tag='events',
new_cardset_tag='extras'
)
if options.expansions:
options.expansions.append("extras")
# Combine all Landmarks across all expansions
if options.exclude_landmarks:
cards = combine_cards(cards,
old_card_type="Landmark",
new_type="Landmarks",
new_card_tag='landmarks',
new_cardset_tag='extras'
)
if options.expansions:
options.expansions.append("extras")
# FIX THIS: Combine all Prizes across all expansions
# if options.exclude_prizes:
# cards = combine_cards(cards, 'Prize', 'prizes')
# Group all the special cards together
if options.special_card_groups:
keep_cards = [] # holds the cards that are to be kept
group_cards = {} # holds the cards for each group
for card in cards:
if not card.group_tag:
keep_cards.append(card) # not part of a group, so just keep the card
else:
# have a card in a group
if card.group_tag not in group_cards:
# First card of a group
group_cards[card.group_tag] = card # save to update cost later
# this card becomes the card holder for the whole group.
card.card_tag = card.group_tag
# These text fields should be updated later if there is a translation for this group_tag.
error_msg = "ERROR: Missing language entry for group_tab '%s'." % card.group_tag
card.name = card.group_tag # For now, change the name to the group_tab
card.description = error_msg
card.extra = error_msg
if card.isEvent():
card.cost = "*"
if card.isLandmark():
card.cost = ""
# now save the card
keep_cards.append(card)
else:
# subsequent cards in the group. Update group info, but don't keep the card.
if card.group_top:
# this is a designated card to represent the group, so update important data
group_cards[card.group_tag].cost = card.cost
group_cards[card.group_tag].potcost = card.potcost
group_cards[card.group_tag].debtcost = card.debtcost
group_cards[card.group_tag].types = card.types
group_cards[card.group_tag].randomizer = card.randomizer
group_cards[card.group_tag].image = card.image
group_cards[card.group_tag].addCardCount(card.count) # increase the count
# group_cards[card.group_tag].set_lowest_cost(card) # set holder to lowest cost of the two cards
cards = keep_cards
# Now fix up card costs
for card in cards:
if card.card_tag in group_cards:
if group_cards[card.group_tag].isEvent():
group_cards[card.group_tag].cost = "*"
group_cards[card.group_tag].debtcost = 0
group_cards[card.group_tag].potcost = 0
if group_cards[card.group_tag].isLandmark():
group_cards[card.group_tag].cost = ""
group_cards[card.group_tag].debtcost = 0
group_cards[card.group_tag].potcost = 0
# Get the final type names in the requested language
Card.type_names = add_type_text(options, Card.type_names, LANGUAGE_DEFAULT)
if options.language != LANGUAGE_DEFAULT:
Card.type_names = add_type_text(options, Card.type_names, options.language)
for card in cards:
card.types_name = ' - '.join([Card.type_names[t] for t in card.types]).upper()
# Get the card bonus keywords in the requested language
bonus = add_bonus_regex(options, LANGUAGE_DEFAULT)
Card.addBonusRegex(bonus)
if options.language != LANGUAGE_DEFAULT:
bonus = add_bonus_regex(options, options.language)
Card.addBonusRegex(bonus)
# Fix up cardset text. Waited as long as possible.
Card.sets = add_set_text(options, Card.sets, LANGUAGE_DEFAULT)
if options.language != LANGUAGE_DEFAULT:
Card.sets = add_set_text(options, Card.sets, options.language)
# Split out Official and Fan set information
Official_sets = set() # Will hold official sets
Official_search = [] # Will hold official sets for searching, both set key and set_name
Fan_sets = set() # Will hold fan sets
Fan_search = [] # Will hold fan sets for searching, both set key and set_name
wantedSets = set() # Will hold all the sets requested for printing
for s in Card.sets:
if Card.sets[s].get("fan", False):
# Fan Expansion
Fan_sets.add(s)
Fan_search.extend([s.lower(), Card.sets[s].get('set_name', None).lower()])
else:
# Official Expansion
Official_sets.add(s)
Official_search.extend([s.lower(), Card.sets[s].get('set_name', None).lower()])
# If expansion names given, then find out which expansions are requested
# Expansion names can be the names from the language or the cardset_tag
if options.expansions:
# Expand out any wildcards, matching set key or set name in the given language
expanded_expansions = []
for e in options.expansions:
matches = fnmatch.filter(Official_search, e)
if matches:
expanded_expansions.extend(matches)
else:
expanded_expansions.append(e)
# Now get the actual sets that are matched above
options.expansions = set([e for e in expanded_expansions]) # Remove duplicates
knownExpansions = set()
for e in options.expansions:
for s in Official_sets:
if (s.lower() == e or Card.sets[s].get('set_name', "").lower() == e):
wantedSets.add(s)
knownExpansions.add(e)
# Give indication if an imput did not match anything
unknownExpansions = options.expansions - knownExpansions
if unknownExpansions:
print(("Error - unknown expansion(s): {}".format(", ".join(unknownExpansions))))
# Take care of fan expansions. Fan expansions must be explicitly named to be added.
# If no --fan is given, then no fan cards are added.
# Fan expansion names can be the names from the language or the cardset_tag
if options.fan:
# Expand out any wildcards, matching set key or set name in the given language
expanded_expansions = []
for e in options.fan:
matches = fnmatch.filter(Fan_search, e)
if matches:
expanded_expansions.extend(matches)
else:
expanded_expansions.append(e)
# Now get the actual sets that are matched above
options.fan = set([e for e in expanded_expansions]) # Remove duplicates
knownExpansions = set()
for e in options.fan:
for s in Fan_sets:
if (s.lower() == e or Card.sets[s].get('set_name', "").lower() == e):
wantedSets.add(s)
knownExpansions.add(e)
# Give indication if an imput did not match anything
unknownExpansions = options.fan - knownExpansions
if unknownExpansions:
print("Error - unknown fan expansion(s): %s" % ", ".join(unknownExpansions))
# Now keep only the cards that are in the sets that have been requested
keep_cards = []
for c in cards:
if c.cardset_tag in wantedSets:
# Add the cardset informaiton to the card and add it to the list of cards to use
c.cardset = Card.sets[c.cardset_tag].get('set_name', c.cardset_tag)
keep_cards.append(c)
cards = keep_cards
# Now add text to the cards. Waited as long as possible to catch all groupings
cards = add_card_text(options, cards, LANGUAGE_DEFAULT)
if options.language != LANGUAGE_DEFAULT:
cards = add_card_text(options, cards, options.language)
# Get list of cards from a file
if options.cardlist:
cardlist = set()
with open(options.cardlist) as cardfile:
for line in cardfile:
cardlist.add(line.strip())
if cardlist:
cards = [card for card in cards if card.name in cardlist]
# Set up the card sorter
cardSorter = CardSorter(
options.order,
{card.card_tag: card.name for card in cards if 'base' in [set_name.lower() for set_name in card.cardset_tags]})
# Optionally remove base cards from expansions that have them
if not options.base_cards_with_expansion:
cards = [card for card in cards
if not cardSorter.isBaseExpansionCard(card)]
# Add expansion divider
if options.expansion_dividers:
cardnamesByExpansion = defaultdict(dict)
randomizerCountByExpansion = Counter()
for c in cards:
if cardSorter.isBaseExpansionCard(c):
continue
if c.randomizer:
randomizerCountByExpansion[c.cardset] += 1
if c.card_tag in cardnamesByExpansion[c.cardset]:
# Already have one, so just update the count (for extra Curses, Start Decks, etc)
cardnamesByExpansion[c.cardset][c.card_tag]['count'] += 1
else:
# New, so save off information about the card to be used on the expansion divider
order = 0
if c.card_tag in cardSorter.baseOrder:
# Use the base card ordering
order = 100 + cardSorter.baseOrder.index(c.card_tag)
cardnamesByExpansion[c.cardset][c.card_tag] = {'name': c.name.strip().replace(' ', '&nbsp;'),
'randomizer': c.randomizer,
'count': 1,
'sort': "%03d%s" % (order, c.name.strip(),)}
for set_tag, set_values in Card.sets.iteritems():
exp = set_values["set_name"]
if exp in cardnamesByExpansion:
exp_name = exp
count = randomizerCountByExpansion[exp]
if 'no_randomizer' in set_values:
if set_values['no_randomizer']:
count = 0
if not options.expansion_dividers_long_name:
if 'short_name' in set_values:
exp_name = set_values['short_name']
card_names = []
for key, n in sorted(cardnamesByExpansion[exp].items(), key=lambda k, x: x['sort']):
if not n['randomizer']:
# Highlight cards without Randomizers
n['name'] = '<i>' + n['name'] + '</i>'
if n['count'] > 1:
# Add number of copies
n['name'] = u"{}&nbsp;\u00d7&nbsp;".format(n['count']) + n['name']
card_names.append(n['name'])
c = Card(name=exp_name,
cardset=exp,
cardset_tag=set_tag,
types=("Expansion", ),
cost=None,
description=' | '.join(card_names),
extra=set_values.get("set_text", ""),
count=count,
card_tag=set_tag)
cards.append(c)
# Now sort what is left
cards.sort(key=cardSorter)
return cards
def calculate_layout(options, cards=[]):
dominionCardWidth, dominionCardHeight = parse_cardsize(options.size,
options.sleeved)
paperwidth, paperheight = parse_papersize(options.papersize)
if options.orientation == "vertical":
dividerWidth, dividerBaseHeight = dominionCardHeight, dominionCardWidth
else:
dividerWidth, dividerBaseHeight = dominionCardWidth, dominionCardHeight
if options.tab_name_align == "center":
options.tab_name_align = "centre"
if options.tab_side == "full" and options.tab_name_align == "edge":
# This case does not make sense since there are two tab edges in this case. So picking left edge.
print("** Warning: Aligning card name as 'left' for 'full' tabs **", file=sys.stderr)
options.tab_name_align = "left"
fixedMargins = False
if options.tabs_only:
# fixed for Avery 8867 for now
minmarginwidth = 0.86 * cm # was 0.76
minmarginheight = 1.37 * cm # was 1.27
labelHeight = 1.07 * cm # was 1.27
labelWidth = 4.24 * cm # was 4.44
horizontalBorderSpace = 0.96 * cm # was 0.76
verticalBorderSpace = 0.20 * cm # was 0.01
dividerBaseHeight = 0
dividerWidth = labelWidth
fixedMargins = True
else:
minmarginwidth, minmarginheight = parseDimensions(options.minmargin)
if options.tab_side == "full":
labelWidth = dividerWidth
else:
labelWidth = options.tabwidth * cm
labelHeight = .9 * cm
horizontalBorderSpace = options.horizontal_gap * cm
verticalBorderSpace = options.vertical_gap * cm
dividerHeight = dividerBaseHeight + labelHeight
dividerWidthReserved = dividerWidth + horizontalBorderSpace
dividerHeightReserved = dividerHeight + verticalBorderSpace
if options.wrapper:
max_card_stack_height = max(c.getStackHeight(options.thickness)
for c in cards)
dividerHeightReserved = (dividerHeightReserved * 2) + (
max_card_stack_height * 2)
print("Max Card Stack Height: {:.2f}cm ".format(max_card_stack_height))
# Notch measurements
notch_height = 0.25 * cm # thumb notch height
notch_width1 = options.notch_length * cm # thumb notch width: top away from tab
notch_width2 = 0.00 * cm # thumb notch width: bottom on side of tab
add_opt(options, 'dividerWidth', dividerWidth)
add_opt(options, 'dividerHeight', dividerHeight)
add_opt(options, 'dividerBaseHeight', dividerBaseHeight)
add_opt(options, 'dividerWidthReserved', dividerWidthReserved)
add_opt(options, 'dividerHeightReserved', dividerHeightReserved)
add_opt(options, 'labelWidth', labelWidth)
add_opt(options, 'labelHeight', labelHeight)
add_opt(options, 'notch_height', notch_height)
add_opt(options, 'notch_width1', notch_width1)
add_opt(options, 'notch_width2', notch_width2)
# as we don't draw anything in the final border, it shouldn't count towards how many tabs we can fit
# so it gets added back in to the page size here
numDividersVerticalP = int(
(paperheight - 2 * minmarginheight + verticalBorderSpace) /
options.dividerHeightReserved)
numDividersHorizontalP = int(
(paperwidth - 2 * minmarginwidth + horizontalBorderSpace) /
options.dividerWidthReserved)
numDividersVerticalL = int(
(paperwidth - 2 * minmarginwidth + verticalBorderSpace) /
options.dividerHeightReserved)
numDividersHorizontalL = int(
(paperheight - 2 * minmarginheight + horizontalBorderSpace) /
options.dividerWidthReserved)
if ((numDividersVerticalL * numDividersHorizontalL > numDividersVerticalP *
numDividersHorizontalP) and not fixedMargins):
add_opt(options, 'numDividersVertical', numDividersVerticalL)
add_opt(options, 'numDividersHorizontal', numDividersHorizontalL)
add_opt(options, 'paperheight', paperwidth)
add_opt(options, 'paperwidth', paperheight)
add_opt(options, 'minHorizontalMargin', minmarginheight)
add_opt(options, 'minVerticalMargin', minmarginwidth)
else:
add_opt(options, 'numDividersVertical', numDividersVerticalP)
add_opt(options, 'numDividersHorizontal', numDividersHorizontalP)
add_opt(options, 'paperheight', paperheight)
add_opt(options, 'paperwidth', paperwidth)
add_opt(options, 'minHorizontalMargin', minmarginheight)
add_opt(options, 'minVerticalMargin', minmarginwidth)
if not fixedMargins:
# dynamically max margins
add_opt(options, 'horizontalMargin',
(options.paperwidth - options.numDividersHorizontal *
options.dividerWidthReserved + horizontalBorderSpace) / 2)
add_opt(options, 'verticalMargin',
(options.paperheight - options.numDividersVertical *
options.dividerHeightReserved + verticalBorderSpace) / 2)
else:
add_opt(options, 'horizontalMargin', minmarginwidth)
add_opt(options, 'verticalMargin', minmarginheight)
def generate(options):
cards = read_card_data(options)
assert cards, "No cards after reading"
cards = filter_sort_cards(cards, options)
assert cards, "No cards after filtering/sorting"
calculate_layout(options, cards)
print("Paper dimensions: {:.2f}cm (w) x {:.2f}cm (h)".format(
options.paperwidth / cm, options.paperheight / cm))
print("Tab dimensions: {:.2f}cm (w) x {:.2f}cm (h)".format(
options.dividerWidthReserved / cm, options.dividerHeightReserved / cm))
print('{} dividers horizontally, {} vertically'.format(
options.numDividersHorizontal, options.numDividersVertical))
print("Margins: {:.2f}cm h, {:.2f}cm v\n".format(
options.horizontalMargin / cm, options.verticalMargin / cm))
dd = DividerDrawer()
dd.draw(cards, options)
def main():
options = parse_opts()
options = clean_opts(options)
if options.preview:
fname = '{}.{}'.format(os.path.splitext(options.outfile)[0], 'png')
open(fname, 'wb').write(generate_sample(options).getvalue())
else:
generate(options)