#!python import re from optparse import OptionParser import os.path import json import codecs from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import LETTER, A4 from reportlab.lib.units import cm from reportlab.platypus import Paragraph from reportlab.lib.styles import getSampleStyleSheet from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfbase import pdfmetrics from reportlab.lib.enums import TA_JUSTIFY def split(l, n): i = 0 while i < len(l) - n: yield l[i:i + n] i += n yield l[i:] class Card(object): class CardJSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, Card): return obj.__dict__ return json.JSONEncoder.default(self, obj) @staticmethod def decode_json(obj): return Card(**obj) def __init__(self, name, cardset, types, cost, description='', potcost=0, extra=''): self.name = name.strip() self.cardset = cardset.strip() self.types = types self.cost = cost self.potcost = potcost self.description = description self.extra = extra def getType(self): return DominionTabs.getType(self.types) def __repr__(self): return '"' + self.name + '"' def toString(self): return self.name + ' ' + self.cardset + ' ' + '-'.join(self.types)\ + ' ' + self.cost + ' ' + self.description + ' ' + self.extra def isExpansion(self): return self.getType().getTypeNames() == ('Expansion',) def isEvent(self): return self.getType().getTypeNames() == ('Event',) def isPrize(self): return 'Prize' in self.getType().getTypeNames() def setImage(self): setImage = DominionTabs.getSetImage(self.cardset, self.name) if setImage is None and self.cardset not in ['base', 'extra'] and not self.isExpansion(): print 'warning, no set image for set "%s" card "%s"' % (self.cardset, self.name) DominionTabs.setImages[self.cardset] = 0 DominionTabs.promoImages[self.name.lower()] = 0 return setImage def setTextIcon(self): setTextIcon = DominionTabs.getSetText(self.cardset, self.name) if setTextIcon is None and self.cardset not in ['base', 'extra'] and not self.isExpansion(): print 'warning, no set text for set "%s" card "%s"' % (self.cardset, self.name) DominionTabs.setTextIcons[self.cardset] = 0 DominionTabs.promoTextIcons[self.name.lower()] = 0 return setTextIcon def isBlank(self): return False class BlankCard(Card): def __init__(self, num): Card.__init__(self, str(num), 'extra', ('Blank',), 0) def isBlank(self): return True class CardType(object): def __init__(self, typeNames, tabImageFile, tabTextHeightOffset=0, tabCostHeightOffset=-1): self.typeNames = typeNames self.tabImageFile = tabImageFile self.tabTextHeightOffset = tabTextHeightOffset self.tabCostHeightOffset = tabCostHeightOffset def getTypeNames(self): return self.typeNames def getTabImageFile(self): if not self.tabImageFile: return None return self.tabImageFile def getNoCoinTabImageFile(self): if not self.tabImageFile: return None return ''.join(os.path.splitext(self.tabImageFile)[0] + '_nc' + os.path.splitext(self.tabImageFile)[1]) def getTabTextHeightOffset(self): return self.tabTextHeightOffset def getTabCostHeightOffset(self): return self.tabCostHeightOffset class DominionTabs: cardTypes = [ CardType(('Action',), 'action.png'), CardType(('Action', 'Attack'), 'action.png'), CardType(('Action', 'Attack', 'Prize'), 'action.png'), CardType(('Action', 'Reaction'), 'reaction.png'), CardType(('Action', 'Victory'), 'action-victory.png'), CardType(('Action', 'Duration'), 'duration.png'), CardType(('Action', 'Duration', 'Reaction'), 'duration-reaction.png'), CardType(('Action', 'Attack', 'Duration'), 'duration.png'), CardType(('Action', 'Looter'), 'action.png'), CardType(('Action', 'Prize'), 'action.png'), CardType(('Action', 'Ruins'), 'ruins.png', 0, 1), CardType(('Action', 'Shelter'), 'action-shelter.png'), CardType(('Action', 'Attack', 'Looter'), 'action.png'), CardType(('Action', 'Attack', 'Traveller'), 'action.png'), CardType(('Action', 'Reserve'), 'reserve.png'), CardType(('Action', 'Reserve', 'Victory'), 'reserve-victory.png'), CardType(('Action', 'Traveller'), 'action.png'), CardType(('Prize',), 'action.png'), CardType(('Event',), 'event.png'), CardType(('Reaction',), 'reaction.png'), CardType(('Reaction', 'Shelter'), 'reaction-shelter.png'), CardType(('Treasure',), 'treasure.png', 3, 0), CardType(('Treasure', 'Attack'), 'treasure.png'), CardType(('Treasure', 'Victory'), 'treasure-victory.png'), CardType(('Treasure', 'Prize'), 'treasure.png', 3, 0), CardType(('Treasure', 'Reaction'), 'treasure-reaction.png', 0, 1), CardType(('Treasure', 'Reserve'), 'reserve-treasure.png'), CardType(('Victory',), 'victory.png'), CardType(('Victory', 'Reaction'), 'victory-reaction.png', 0, 1), CardType(('Victory', 'Shelter'), 'victory-shelter.png'), CardType(('Curse',), 'curse.png', 3), CardType(('Expansion',), 'expansion.png', 4), CardType(('Blank',), '') ] cardTypes = dict(((c.getTypeNames(), c) for c in cardTypes)) language_mapping = None @classmethod def getType(cls, typespec): return cls.cardTypes[tuple(typespec)] setImages = { 'dominion': 'base_set.png', 'intrigue': 'intrigue_set.png', 'seaside': 'seaside_set.png', 'prosperity': 'prosperity_set.png', 'alchemy': 'alchemy_set.png', 'cornucopia': 'cornucopia_set.png', 'cornucopia extras': 'cornucopia_set.png', 'hinterlands': 'hinterlands_set.png', 'dark ages': 'dark_ages_set.png', 'dark ages extras': 'dark_ages_set.png', 'guilds': 'guilds_set.png', 'adventures': 'adventures_set.png', 'adventures extras': 'adventures_set.png' } promoImages = { 'walled village': 'walled_village_set.png', 'stash': 'stash_set.png', 'governor': 'governor_set.png', 'black market': 'black_market_set.png', 'envoy': 'envoy_set.png', 'prince': 'prince_set.png' } setTextIcons = { 'dominion': 'D', 'intrigue': 'I', 'seaside': 'S', 'prosperity': 'P', 'alchemy': 'A', 'cornucopia': 'C', 'cornucopia extras': 'C', 'hinterlands': 'H', 'dark ages': 'DA', 'dark ages extras': 'DA', 'guilds': 'G', 'adventures': 'Ad', 'adventures extras': 'Ad' } promoTextIcons = { 'walled village': '', 'stash': '', 'governor': '', 'black market': '', 'envoy': '', 'prince': '' } @classmethod def getSetImage(cls, setName, cardName): if setName in cls.setImages: return cls.setImages[setName] if cardName.lower() in cls.promoImages: return cls.promoImages[cardName.lower()] if setName in cls.language_mapping: trans = cls.language_mapping[setName] if trans in cls.setImages: return cls.setImages[trans] if cardName in cls.language_mapping: trans = cls.language_mapping[cardName] if trans.lower() in cls.promoImages: return cls.promoImages[trans.lower()] return None @classmethod def getSetText(cls, setName, cardName): if setName in cls.setTextIcons: return cls.setTextIcons[setName] if cardName.lower() in cls.promoTextIcons: return cls.promoTextIcons[cardName.lower()] return None def __init__(self): self.filedir = os.path.dirname(__file__) self.odd = True def add_inline_images(self, text, fontsize): path = os.path.join(self.filedir, 'images') replace = '' % ( path, fontsize * 1.2) text = re.sub('(\d)\s(c|C)oin(s)?', replace, text) replace = '' % ( path, fontsize * 1.2) text = re.sub('\?\s(c|C)oin(s)?', replace, text) replace = '' % ( path, fontsize * 1.5) text = re.sub('\', replace, text) return text def drawOutline(self, x, y, rightSide, isBack=False, isExpansionDivider=False): # draw outline or cropmarks self.canvas.saveState() self.canvas.setLineWidth(self.options.linewidth) cropmarksright = (x == self.numTabsHorizontal - 1) cropmarksleft = (x == 0) if rightSide: self.canvas.translate(self.tabWidth, 0) self.canvas.scale(-1, 1) if not self.options.cropmarks and not isBack: # don't draw outline on back, in case lines don't line up with # front if isExpansionDivider and self.options.centre_expansion_dividers: outline = self.expansionTabOutline else: outline = self.tabOutline self.canvas.lines(outline) elif self.options.cropmarks: cmw = 0.5 * cm # Horizontal-line cropmarks mirror = cropmarksright and not rightSide or cropmarksleft and rightSide if mirror: self.canvas.saveState() self.canvas.translate(self.tabWidth, 0) self.canvas.scale(-1, 1) if cropmarksleft or cropmarksright: self.canvas.line(-2 * cmw, 0, -cmw, 0) self.canvas.line(-2 * cmw, self.tabBaseHeight, -cmw, self.tabBaseHeight) if y > 0: self.canvas.line(-2 * cmw, self.tabHeight, -cmw, self.tabHeight) if mirror: self.canvas.restoreState() # Vertical-line cropmarks # want to always draw the right-edge and middle-label-edge lines.. # ...and draw the left-edge if this is the first card on the left # ...but we need to take mirroring into account, to know "where" # to draw the left / right lines... if rightSide: leftLine = self.tabWidth rightLine = 0 else: leftLine = 0 rightLine = self.tabWidth middleLine = self.tabWidth - self.tabLabelWidth if y == 0: self.canvas.line(rightLine, -2 * cmw, rightLine, -cmw) self.canvas.line(middleLine, -2 * cmw, middleLine, -cmw) if cropmarksleft: self.canvas.line(leftLine, -2 * cmw, leftLine, -cmw) if y == self.numTabsVertical - 1: self.canvas.line(rightLine, self.tabHeight + cmw, rightLine, self.tabHeight + 2 * cmw) self.canvas.line(middleLine, self.tabHeight + cmw, middleLine, self.tabHeight + 2 * cmw) if cropmarksleft: self.canvas.line(leftLine, self.tabHeight + cmw, leftLine, self.tabHeight + 2 * cmw) self.canvas.restoreState() def drawCost(self, card, x, y, costOffset=-1): # base width is 16 (for image) + 2 (1 pt border on each side) width = 18 costHeight = y + costOffset coinHeight = costHeight - 5 potHeight = y - 3 potSize = 11 self.canvas.drawImage(os.path.join(self.filedir, 'images', 'coin_small.png'), x, coinHeight, 16, 16, preserveAspectRatio=True, mask='auto') if card.potcost: self.canvas.drawImage(os.path.join(self.filedir, 'images', 'potion.png'), x + 17, potHeight, potSize, potSize, preserveAspectRatio=True, mask=[255, 255, 255, 255, 255, 255]) width += potSize self.canvas.setFont('MinionPro-Bold', 12) cost = str(card.cost) self.canvas.drawCentredString(x + 8, costHeight, cost) return width def drawSetIcon(self, setImage, x, y): # set image self.canvas.drawImage( os.path.join(self.filedir, 'images', setImage), x, y, 14, 12, mask='auto') @classmethod def nameWidth(self, name, fontSize): w = 0 name_parts = name.split() for i, part in enumerate(name_parts): if i != 0: w += pdfmetrics.stringWidth(' ', 'MinionPro-Regular', fontSize) w += pdfmetrics.stringWidth(part[0], 'MinionPro-Regular', fontSize) w += pdfmetrics.stringWidth(part[1:], 'MinionPro-Regular', fontSize - 2) return w def drawTab(self, card, rightSide): # draw tab flap self.canvas.saveState() if card.isExpansion() and self.options.centre_expansion_dividers: self.canvas.translate(self.tabWidth / 2 - self.tabLabelWidth / 2, self.tabHeight - self.tabLabelHeight) elif not rightSide: self.canvas.translate(self.tabWidth - self.tabLabelWidth, self.tabHeight - self.tabLabelHeight) else: self.canvas.translate(0, self.tabHeight - self.tabLabelHeight) # allow for 3 pt border on each side textWidth = self.tabLabelWidth - 6 textHeight = 7 if self.options.no_tab_artwork: textHeight = 4 textHeight = self.tabLabelHeight / 2 - textHeight + \ card.getType().getTabTextHeightOffset() # draw banner img = card.getType().getNoCoinTabImageFile() if not self.options.no_tab_artwork and img: self.canvas.drawImage(os.path.join(self.filedir, 'images', img), 1, 0, self.tabLabelWidth - 2, self.tabLabelHeight - 1, preserveAspectRatio=False, anchor='n', mask='auto') # draw cost if not card.isExpansion() and not card.isBlank(): if 'tab' in self.options.cost: textInset = 4 textInset += self.drawCost(card, textInset, textHeight, card.getType().getTabCostHeightOffset()) else: textInset = 6 else: textInset = 13 # draw set image # always need to offset from right edge, to make sure it stays on # banner textInsetRight = 6 if self.options.use_text_set_icon: setImageHeight = card.getType().getTabTextHeightOffset() setText = card.setTextIcon() self.canvas.setFont('MinionPro-Oblique', 8) if setText is None: setText = "" self.canvas.drawCentredString(self.tabLabelWidth - 10, textHeight + 2, setText) textInsetRight = 15 else: setImage = card.setImage() if setImage and 'tab' in self.options.set_icon: setImageHeight = 3 + card.getType().getTabTextHeightOffset() self.drawSetIcon(setImage, self.tabLabelWidth - 20, setImageHeight) textInsetRight = 20 # draw name fontSize = 12 name = card.name.upper() textWidth -= textInset textWidth -= textInsetRight width = self.nameWidth(name, fontSize) while width > textWidth and fontSize > 8: fontSize -= .01 # print 'decreasing font size for tab of',name,'now',fontSize width = self.nameWidth(name, fontSize) tooLong = width > textWidth if tooLong: name_lines = name.partition(' / ') if name_lines[1]: name_lines = (name_lines[0] + ' /', name_lines[2]) else: name_lines = name.split(None, 1) else: name_lines = [name] # if tooLong: # print name for linenum, line in enumerate(name_lines): h = textHeight if tooLong and len(name_lines) > 1: if linenum == 0: h += h / 2 else: h -= h / 2 words = line.split() if (not self.options.tab_name_align == "right") and (self.options.tab_name_align == "centre" or rightSide or not self.options.tab_name_align == "edge"): if self.options.tab_name_align == "centre": w = self.tabLabelWidth / 2 - self.nameWidth(line, fontSize) / 2 else: w = textInset def drawWordPiece(text, fontSize): self.canvas.setFont('MinionPro-Regular', fontSize) if text != ' ': self.canvas.drawString(w, h, text) return pdfmetrics.stringWidth(text, 'MinionPro-Regular', fontSize) for i, word in enumerate(words): if i != 0: w += drawWordPiece(' ', fontSize) w += drawWordPiece(word[0], fontSize) w += drawWordPiece(word[1:], fontSize - 2) else: # align text to the right if tab is on right side, to make # tabs easier to read when grouped together extra 3pt is for # space between text + set symbol w = self.tabLabelWidth - textInsetRight - 3 words.reverse() def drawWordPiece(text, fontSize): self.canvas.setFont('MinionPro-Regular', fontSize) if text != ' ': self.canvas.drawRightString(w, h, text) return -pdfmetrics.stringWidth(text, 'MinionPro-Regular', fontSize) for i, word in enumerate(words): w += drawWordPiece(word[1:], fontSize - 2) w += drawWordPiece(word[0], fontSize) if i != len(words) - 1: w += drawWordPiece(' ', fontSize) self.canvas.restoreState() def drawText(self, card, useExtra=False): usedHeight = 0 totalHeight = self.tabHeight - self.tabLabelHeight drewTopIcon = False if 'body-top' in self.options.cost and not card.isExpansion(): self.drawCost(card, cm / 4.0, totalHeight - 0.5 * cm) drewTopIcon = True if 'body-top' in self.options.set_icon and not card.isExpansion(): setImage = card.setImage() if setImage: self.drawSetIcon(setImage, self.tabWidth - 16, totalHeight - 0.5 * cm - 3) drewTopIcon = True if drewTopIcon: usedHeight += 15 if self.options.no_card_rules: return # draw text if useExtra and card.extra: descriptions = (card.extra,) else: descriptions = re.split("\n", card.description) s = getSampleStyleSheet()['BodyText'] s.fontName = "Times-Roman" s.alignment = TA_JUSTIFY textHorizontalMargin = .5 * cm textVerticalMargin = .3 * cm textBoxWidth = self.tabWidth - 2 * textHorizontalMargin textBoxHeight = totalHeight - usedHeight - 2 * textVerticalMargin spacerHeight = 0.2 * cm minSpacerHeight = 0.05 * cm while True: paragraphs = [] # this accounts for the spacers we insert between paragraphs h = (len(descriptions) - 1) * spacerHeight for d in descriptions: dmod = self.add_inline_images(d, s.fontSize) p = Paragraph(dmod, s) h += p.wrap(textBoxWidth, textBoxHeight)[1] paragraphs.append(p) if h <= textBoxHeight or s.fontSize <= 1 or s.leading <= 1: break else: s.fontSize -= 1 s.leading -= 1 spacerHeight = max(spacerHeight - 1, minSpacerHeight) h = totalHeight - usedHeight - textVerticalMargin for p in paragraphs: h -= p.height p.drawOn(self.canvas, textHorizontalMargin, h) h -= spacerHeight def drawDivider(self, card, x, y, useExtra=False): # figure out whether the tab should go on the right side or not if self.options.tab_side == "right": rightSide = useExtra elif self.options.tab_side in ["left", "full"]: rightSide = not useExtra else: # alternate the cards if not useExtra: rightSide = not self.odd else: rightSide = self.odd # apply the transforms to get us to the corner of the current card self.canvas.resetTransforms() self.canvas.translate(self.horizontalMargin, self.verticalMargin) if useExtra: self.canvas.translate(self.options.back_offset, self.options.back_offset_height) self.canvas.translate(x * self.totalTabWidth, y * self.totalTabHeight) # actual drawing if not self.options.tabs_only: self.drawOutline( x, y, rightSide, useExtra, card.getType().getTypeNames() == ('Expansion',)) self.drawTab(card, rightSide) if not self.options.tabs_only: self.drawText(card, useExtra) def drawSetNames(self, pageCards): # print sets for this page self.canvas.saveState() try: # calculate the text height, font size, and orientation maxFontsize = 12 minFontsize = 6 fontname = 'MinionPro-Regular' font = pdfmetrics.getFont(fontname) fontHeightRelative = (font.face.ascent + abs(font.face.descent)) / 1000.0 canFit = False layouts = [{'rotation': 0, 'minMarginHeight': self.minVerticalMargin, 'totalMarginHeight': self.verticalMargin, 'width': self.paperwidth}, {'rotation': 90, 'minMarginHeight': self.minHorizontalMargin, 'totalMarginHeight': self.horizontalMargin, 'width': self.paperheight}] for layout in layouts: availableMargin = layout[ 'totalMarginHeight'] - layout['minMarginHeight'] fontsize = availableMargin / fontHeightRelative fontsize = min(maxFontsize, fontsize) if fontsize >= minFontsize: canFit = True break if not canFit: import warnings warnings.warn("Not enough space to display set names") return self.canvas.setFont(fontname, fontsize) sets = [] for c in pageCards: setTitle = c.cardset.title() if setTitle not in sets: sets.append(setTitle) # Centered on page xPos = layout['width'] / 2 # Place at the very edge of the margin yPos = layout['minMarginHeight'] if layout['rotation']: self.canvas.rotate(layout['rotation']) yPos = -yPos self.canvas.drawCentredString(xPos, yPos, '/'.join(sets)) finally: self.canvas.restoreState() def drawDividers(self, cards): # split into pages cards = split(cards, self.numTabsVertical * self.numTabsHorizontal) # Starting with tabs on the left or the right? if self.options.tab_side in ["right-alternate", "right"]: self.odd = True else: # left-alternate, left, full self.odd = False for pageNum, pageCards in enumerate(cards): # remember whether we start with odd or even divider for tab # location pageStartOdd = self.odd if not self.options.no_page_footer and (not self.options.tabs_only and self.options.order != "global"): self.drawSetNames(pageCards) for i, card in enumerate(pageCards): # print card x = i % self.numTabsHorizontal y = i / self.numTabsHorizontal self.canvas.saveState() self.drawDivider(card, x, self.numTabsVertical - 1 - y) self.canvas.restoreState() self.odd = not self.odd self.canvas.showPage() if pageNum + 1 == self.options.num_pages: break if self.options.tabs_only or self.options.no_card_backs: # no set names or card backs for label-only sheets continue if not self.options.no_page_footer and self.options.order != "global": self.drawSetNames(pageCards) # start at same oddness self.odd = pageStartOdd for i, card in enumerate(pageCards): # print card x = (self.numTabsHorizontal - 1 - i) % self.numTabsHorizontal y = i / self.numTabsHorizontal self.canvas.saveState() self.drawDivider( card, x, self.numTabsVertical - 1 - y, useExtra=True) self.canvas.restoreState() self.odd = not self.odd self.canvas.showPage() if pageNum + 1 == self.options.num_pages: break LOCATION_CHOICES = ["tab", "body-top", "hide"] NAME_ALIGN_CHOICES = ["left", "right", "centre", "edge"] TAB_SIDE_CHOICES = ["left", "right", "left-alternate", "right-alternate", "full"] @classmethod def parse_opts(cls, argstring): parser = OptionParser() parser.add_option("--back_offset", type="float", dest="back_offset", default=0, help="Points to offset the back page to the right; needed for some printers") parser.add_option("--back_offset_height", type="float", dest="back_offset_height", default=0, help="Points to offset the back page upward; needed for some printers") parser.add_option("--orientation", type="choice", choices=["horizontal", "vertical"], dest="orientation", default="horizontal", help="horizontal or vertical, default:horizontal") parser.add_option("--sleeved", action="store_true", dest="sleeved", help="use --size=sleeved instead") parser.add_option("--size", type="string", dest="size", default='normal', help="'<%f>x<%f>' (size in cm), or 'normal' = '9.1x5.9', or 'sleeved' = '9.4x6.15'") parser.add_option("--minmargin", type="string", dest="minmargin", default="1x1", help="'<%f>x<%f>' (size in cm, left/right, top/bottom), default: 1x1") parser.add_option("--papersize", type="string", dest="papersize", default=None, help="'<%f>x<%f>' (size in cm), or 'A4', or 'LETTER'") parser.add_option("--tab_name_align", type="choice", choices=DominionTabs.NAME_ALIGN_CHOICES + ["center"], dest="tab_name_align", default="left", help="Alignment of text on the tab. choices: left, right, centre (or center), edge." " 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);" " default:left") parser.add_option("--tab_side", type="choice", choices=DominionTabs.TAB_SIDE_CHOICES, dest="tab_side", default="right-alternate", help="Alignment of tab. choices: left, right, left-alternate, right-alternate, full;" " 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;" # noqa " full will force all label tabs to be full width of the divider" " default:right-alternate") parser.add_option("--tabwidth", type="float", default=4, help="width in cm of stick-up tab (ignored if tab_side is full or tabs-only is used)") parser.add_option("--cost", action="append", type="choice", choices=cls.LOCATION_CHOICES, default=[], 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; valid values are: %s; defaults to 'tab'" % ", ".join("'%s'" % x for x in cls.LOCATION_CHOICES)) parser.add_option("--set_icon", action="append", type="choice", choices=cls.LOCATION_CHOICES, default=[], 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; valid values are: %s; defaults to 'tab'" % ", ".join("'%s'" % x for x in cls.LOCATION_CHOICES)) parser.add_option("--expansions", action="append", type="string", help="subset of dominion expansions to produce tabs for") parser.add_option("--cropmarks", action="store_true", dest="cropmarks", help="print crop marks on both sides, rather than tab outlines on one") parser.add_option("--linewidth", type="float", default=.1, help="width of lines for card outlines/crop marks") parser.add_option("--write_json", action="store_true", dest="write_json", help="write json version of card definitions and extras") parser.add_option("--tabs-only", action="store_true", dest="tabs_only", help="draw only tabs to be printed on labels, no divider outlines") parser.add_option("--order", type="choice", choices=["expansion", "global"], dest="order", help="sort order for the cards, whether by expansion or globally alphabetical") parser.add_option("--expansion_dividers", action="store_true", dest="expansion_dividers", help="add dividers describing each expansion set") parser.add_option("--base_cards_with_expansion", action="store_true", help='print the base cards as part of the expansion; ie, a divider for "Silver"' ' will be printed as both a "Dominion" card and as an "Intrigue" card; if this' ' option is not given, all base cards are placed in their own "Base" expansion') parser.add_option("--centre_expansion_dividers", action="store_true", dest="centre_expansion_dividers", help='centre the tabs on expansion dividers') parser.add_option("--num_pages", type="int", default=-1, help="stop generating after this many pages, -1 for all") parser.add_option("--language", default='en_us', help="language of card texts") parser.add_option("--include_blanks", action="store_true", help="include a few dividers with extra text") parser.add_option("--exclude_events", action="store_true", default=False, help="exclude individual dividers for events") parser.add_option("--special_card_groups", action="store_true", default=False, help="group some cards under special dividers (e.g. Shelters, Prizes)") parser.add_option("--exclude_prizes", action="store_true", default=False, help="exclude individual dividers for prizes (cornucopia)") parser.add_option("--cardlist", type="string", dest="cardlist", default=None, help="Path to file that enumerates each card to be printed on its own line.") parser.add_option("--no-tab-artwork", action="store_true", dest="no_tab_artwork", help="don't show background artwork on tabs") parser.add_option("--no-card-rules", action="store_true", dest="no_card_rules", help="don't print the card's rules on the tab body") parser.add_option("--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") parser.add_option("--no-page-footer", action="store_true", dest="no_page_footer", help="don't print the set name at the bottom of the page.") parser.add_option("--no-card-backs", action="store_true", dest="no_card_backs", help="don't print the back page of the card sheets.") options, args = parser.parse_args(argstring) if not options.cost: options.cost = ['tab'] if not options.set_icon: options.set_icon = ['tab'] return options, args def main(self, argstring): options, args = DominionTabs.parse_opts(argstring) fname = None if args: fname = args[0] return self.generate(options, fname) def parseDimensions(self, dimensionsStr): x, y = dimensionsStr.upper().split('X', 1) return (float(x) * cm, float(y) * cm) def generate_sample(self, options): import cStringIO from wand.image import Image buf = cStringIO.StringIO() options.num_pages = 1 self.generate(options, buf) with Image(blob=buf.getvalue()) as sample: sample.format = 'png' sample.save(filename='sample.png') def generate(self, options, f): self.options = options size = self.options.size.upper() if size == 'SLEEVED' or self.options.sleeved: dominionCardWidth, dominionCardHeight = (9.4 * cm, 6.15 * cm) print 'Using sleeved card size, %.2fcm x %.2fcm' % (dominionCardWidth / cm, dominionCardHeight / cm) elif size in ['NORMAL', 'UNSLEEVED']: dominionCardWidth, dominionCardHeight = (9.1 * cm, 5.9 * cm) print 'Using normal card size, %.2fcm x%.2fcm' % (dominionCardWidth / cm, dominionCardHeight / cm) else: dominionCardWidth, dominionCardHeight = self.parseDimensions(size) print 'Using custom card size, %.2fcm x %.2fcm' % (dominionCardWidth / cm, dominionCardHeight / cm) papersize = None if not self.options.papersize: if os.path.exists("/etc/papersize"): papersize = open("/etc/papersize").readline().upper() else: papersize = 'LETTER' else: papersize = self.options.papersize.upper() if papersize == 'A4': print "Using A4 sized paper." self.paperwidth, self.paperheight = A4 elif papersize == 'LETTER': print "Using letter sized paper." self.paperwidth, self.paperheight = LETTER else: self.paperwidth, self.paperheight = self.parseDimensions(papersize) print 'Using custom paper size, %.2fcm x %.2fcm' % (self.paperwidth / cm, self.paperheight / cm) self.cardlist = None if self.options.cardlist: print self.options.cardlist self.cardlist = set() with open(self.options.cardlist) as cardfile: for line in cardfile: self.cardlist.add(line.strip()) if self.options.orientation == "vertical": self.tabWidth, self.tabBaseHeight = dominionCardHeight, dominionCardWidth else: self.tabWidth, self.tabBaseHeight = dominionCardWidth, dominionCardHeight if self.options.tab_name_align == "center": self.options.tab_name_align = "centre" if self.options.tab_side == "full" and self.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 >>sys.stderr, "** Warning: Aligning card name as 'left' for 'full' tabs **" self.options.tab_name_align == "left" fixedMargins = False if self.options.tabs_only: # fixed for Avery 8867 for now minmarginwidth = 0.86 * cm # was 0.76 minmarginheight = 1.37 * cm # was 1.27 self.tabLabelHeight = 1.07 * cm # was 1.27 self.tabLabelWidth = 4.24 * cm # was 4.44 self.horizontalBorderSpace = 0.96 * cm # was 0.76 self.verticalBorderSpace = 0.20 * cm # was 0.01 self.tabBaseHeight = 0 self.tabWidth = self.tabLabelWidth fixedMargins = True else: minmarginwidth, minmarginheight = self.parseDimensions( self.options.minmargin) if self.options.tab_side == "full": self.tabLabelWidth = self.tabWidth else: self.tabLabelWidth = self.options.tabwidth * cm self.tabLabelHeight = .9 * cm self.horizontalBorderSpace = 0 * cm self.verticalBorderSpace = 0 * cm self.tabHeight = self.tabBaseHeight + self.tabLabelHeight # note: this is convenient, but somewhat inaccurate as the border space # isn't actually part of the tab width self.totalTabWidth = self.tabWidth + self.horizontalBorderSpace self.totalTabHeight = self.tabHeight + self.verticalBorderSpace # 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 numTabsVerticalP = int( (self.paperheight - 2 * minmarginheight + self.verticalBorderSpace) / self.totalTabHeight) numTabsHorizontalP = int( (self.paperwidth - 2 * minmarginwidth + self.horizontalBorderSpace) / self.totalTabWidth) numTabsVerticalL = int( (self.paperwidth - 2 * minmarginwidth + self.verticalBorderSpace) / self.totalTabHeight) numTabsHorizontalL = int( (self.paperheight - 2 * minmarginheight + self.horizontalBorderSpace) / self.totalTabWidth) if numTabsVerticalL * numTabsHorizontalL > numTabsVerticalP * numTabsHorizontalP and not fixedMargins: self.numTabsVertical, self.numTabsHorizontal\ = numTabsVerticalL, numTabsHorizontalL self.paperheight, self.paperwidth = self.paperwidth, self.paperheight self.minHorizontalMargin = minmarginheight self.minVerticalMargin = minmarginwidth else: self.numTabsVertical, self.numTabsHorizontal\ = numTabsVerticalP, numTabsHorizontalP self.minHorizontalMargin = minmarginwidth self.minVerticalMargin = minmarginheight print "Paper dimensions: {:.2f}cm (w) x {:.2f}cm (h)".format(self.paperwidth / cm, self.paperheight / cm) print "Tab dimensions: {:.2f}cm (w) x {:.2f}cm (h)".format(self.totalTabWidth / cm, self.totalTabHeight / cm) print '{} dividers horizontally, {} vertically'.format(self.numTabsHorizontal, self.numTabsVertical) if not fixedMargins: # dynamically max margins self.horizontalMargin = ( self.paperwidth - self.numTabsHorizontal * self.totalTabWidth) / 2 self.verticalMargin = ( self.paperheight - self.numTabsVertical * self.totalTabHeight) / 2 else: self.horizontalMargin = minmarginwidth self.verticalMargin = minmarginheight print "Margins: {:.2f}cm h, {:.2f}cm v\n".format(self.horizontalMargin / cm, self.verticalMargin / cm) self.tabOutline = [(0, 0, self.tabWidth, 0), (self.tabWidth, 0, self.tabWidth, self.tabHeight), (self.tabWidth, self.tabHeight, self.tabWidth - self.tabLabelWidth, self.tabHeight), (self.tabWidth - self.tabLabelWidth, self.tabHeight, self.tabWidth - self.tabLabelWidth, self.tabBaseHeight), (self.tabWidth - self.tabLabelWidth, self.tabBaseHeight, 0, self.tabBaseHeight), (0, self.tabBaseHeight, 0, 0)] self.expansionTabOutline = [(0, 0, self.tabWidth, 0), (self.tabWidth, 0, self.tabWidth, self.tabBaseHeight), (self.tabWidth, self.tabBaseHeight, self.tabWidth / 2 + self.tabLabelWidth / 2, self.tabBaseHeight), (self.tabWidth / 2 + self.tabLabelWidth / 2, self.tabBaseHeight, self.tabWidth / 2 + self.tabLabelWidth / 2, self.tabHeight), (self.tabWidth / 2 + self.tabLabelWidth / 2, self.tabHeight, self.tabWidth / 2 - self.tabLabelWidth / 2, self.tabHeight), (self.tabWidth / 2 - self.tabLabelWidth / 2, self.tabHeight, self.tabWidth / 2 - self.tabLabelWidth / 2, self.tabBaseHeight), (self.tabWidth / 2 - self.tabLabelWidth / 2, self.tabBaseHeight, 0, self.tabBaseHeight), (0, self.tabBaseHeight, 0, 0)] try: dirn = os.path.join(self.filedir, 'fonts') pdfmetrics.registerFont( TTFont('MinionPro-Regular', os.path.join(dirn, 'MinionPro-Regular.ttf'))) pdfmetrics.registerFont( TTFont('MinionPro-Bold', os.path.join(dirn, 'MinionPro-Bold.ttf'))) pdfmetrics.registerFont( TTFont('MinionPro-Oblique', os.path.join(dirn, 'MinionPro-It.ttf'))) except: raise pdfmetrics.registerFont( TTFont('MinionPro-Regular', 'OptimusPrincepsSemiBold.ttf')) pdfmetrics.registerFont( TTFont('MinionPro-Bold', 'OptimusPrinceps.ttf')) data_dir = os.path.join(self.filedir, "card_db", options.language) card_db_filepath = os.path.join(data_dir, "cards.json") with codecs.open(card_db_filepath, "r", "utf-8") as cardfile: cards = json.load(cardfile, object_hook=Card.decode_json) language_mapping_filepath = os.path.join(data_dir, "mapping.json") with codecs.open(language_mapping_filepath, 'r', 'utf-8') as mapping_file: DominionTabs.language_mapping = json.load(mapping_file) baseCards = [ card.name for card in cards if card.cardset.lower() == 'base'] def isBaseExpansionCard(card): return card.cardset.lower() != 'base' and card.name in baseCards if self.options.base_cards_with_expansion: cards = [card for card in cards if card.cardset.lower() != 'base'] else: cards = [card for card in cards if not isBaseExpansionCard(card)] if self.options.special_card_groups: # Load the card groups file card_groups_file = os.path.join(data_dir, "card_groups.json") with codecs.open(card_groups_file, 'r', 'utf-8') as cardgroup_file: card_groups = json.load(cardgroup_file) # pull out any cards which are a subcard, and rename the master card new_cards = [] all_subcards = [] for subs in [card_groups[x]["subcards"] for x in card_groups]: all_subcards += subs for card in cards: if card.name in card_groups.keys(): card.name = card_groups[card.name]["new_name"] elif card.name in all_subcards: continue new_cards.append(card) cards = new_cards if self.options.expansions: self.options.expansions = [o.lower() for o in self.options.expansions] reverseMapping = { v: k for k, v in DominionTabs.language_mapping.iteritems()} self.options.expansions = [ reverseMapping.get(e, e) for e in self.options.expansions] filteredCards = [] knownExpansions = set() for c in cards: knownExpansions.add(c.cardset) if next((e for e in self.options.expansions if c.cardset.startswith(e)), None): filteredCards.append(c) unknownExpansions = set(self.options.expansions) - knownExpansions if unknownExpansions: print "Error - unknown expansion(s): %s" % ", ".join(unknownExpansions) return cards = filteredCards if self.options.exclude_events: cards = [card for card in cards if not card.isEvent() or card.name == 'Events'] if self.options.exclude_prizes: cards = [card for card in cards if not card.isPrize()] if self.cardlist: cards = [card for card in cards if card.name in self.cardlist] if options.expansion_dividers: cardnamesByExpansion = {} for c in cards: if isBaseExpansionCard(c): continue cardnamesByExpansion.setdefault( c.cardset, []).append(c.name.strip()) for exp, names in cardnamesByExpansion.iteritems(): c = Card( exp, exp, ("Expansion",), None, ' | '.join(sorted(names))) cards.append(c) if options.write_json: fpath = os.path.join(self.filedir, "card_db", options.language, "cards.json") with codecs.open(fpath, 'w', encoding='utf-8') as ofile: json.dump(cards, ofile, cls=Card.CardJSONEncoder, ensure_ascii=False, indent=True, sort_keys=True) # When sorting cards, want to always put "base" cards after all # kingdom cards, and order the base cards in a set order - the # order they are listed in the database (ie, all normal treasures # by worth, then potion, then all normal VP cards by worth, then # trash) def baseIndex(name): try: return baseCards.index(name) except Exception: return -1 if options.order == "global": sortKey = lambda x: ( int(x.isExpansion()), baseIndex(x.name), x.name) else: sortKey = lambda x: ( x.cardset, int(x.isExpansion()), baseIndex(x.name), x.name) cards.sort(key=sortKey) if not f: f = "dominion_tabs.pdf" self.canvas = canvas.Canvas( f, pagesize=(self.paperwidth, self.paperheight)) self.drawDividers(cards) self.canvas.save() if __name__ == '__main__': import sys tabs = DominionTabs() tabs.main(sys.argv[1:])