Wendel Voigt d024dc9fb4 Added front and back text options
1. Added --front and --back to select the type of text to display.
Options are 'card' for the card text, 'rules' for the extra rules,
'blank' to not print anything.  And for --back, the option 'none' will
keep the pages with the backs from printing.
2. Added stub test cases for card text as well as tab size and text
alignment.
3. removed options --no-card-rules and --no-card-backs since they are no
longer needed.
4. Fixed issue with the tab outline drawing
2015-11-24 10:21:47 -06:00

541 lines
23 KiB
Python

import os
import re
import sys
from reportlab.lib.units import cm
from reportlab.pdfbase import pdfmetrics
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph
from reportlab.lib.enums import TA_JUSTIFY
from reportlab.pdfgen import canvas
from reportlab.pdfbase.ttfonts import TTFont
def split(l, n):
i = 0
while i < len(l) - n:
yield l[i:i + n]
i += n
yield l[i:]
class DividerDrawer(object):
def __init__(self):
self.odd = True
self.canvas = None
def registerFonts(self):
try:
dirn = os.path.join(self.options.data_path, 'fonts')
self.fontNameRegular = 'MinionPro-Regular'
pdfmetrics.registerFont(
TTFont(self.fontNameRegular, os.path.join(dirn, 'MinionPro-Regular.ttf')))
self.fontNameBold = 'MinionPro-Bold'
pdfmetrics.registerFont(
TTFont(self.fontNameBold, os.path.join(dirn, 'MinionPro-Bold.ttf')))
self.fontNameOblique = 'MinionPro-Oblique'
pdfmetrics.registerFont(
TTFont(self.fontNameOblique, os.path.join(dirn, 'MinionPro-It.ttf')))
except:
print >>sys.stderr, "Warning, Minion Pro Font ttf file not found! Falling back on Times"
self.fontNameRegular = 'Times-Roman'
self.fontNameBold = 'Times-Bold'
self.fontNameOblique = 'Times-Oblique'
def draw(self, fname, cards, options):
self.options = options
self.registerFonts()
dividerWidth = options.dividerWidth
dividerHeight = options.dividerHeight
dividerBaseHeight = options.dividerBaseHeight
tabLabelWidth = options.labelWidth
self.tabOutline = [(0, 0, dividerWidth, 0),
(dividerWidth, 0, dividerWidth, dividerHeight),
(dividerWidth, dividerHeight,
dividerWidth - tabLabelWidth, dividerHeight),
(dividerWidth - tabLabelWidth,
dividerHeight, dividerWidth - tabLabelWidth,
dividerBaseHeight),
(dividerWidth - tabLabelWidth,
dividerBaseHeight, 0, dividerBaseHeight),
(0, dividerBaseHeight, 0, 0)]
self.expansionTabOutline = [(0, 0, dividerWidth, 0),
(dividerWidth, 0, dividerWidth,
dividerBaseHeight),
(dividerWidth, dividerBaseHeight,
dividerWidth / 2 + tabLabelWidth / 2, dividerBaseHeight),
(dividerWidth / 2 + tabLabelWidth / 2, dividerBaseHeight,
dividerWidth / 2 + tabLabelWidth / 2, dividerHeight),
(dividerWidth / 2 + tabLabelWidth / 2, dividerHeight,
dividerWidth / 2 - tabLabelWidth / 2, dividerHeight),
(dividerWidth / 2 - tabLabelWidth / 2, dividerHeight,
dividerWidth / 2 - tabLabelWidth / 2, dividerBaseHeight),
(dividerWidth / 2 - tabLabelWidth / 2, dividerBaseHeight,
0, dividerBaseHeight),
(0, dividerBaseHeight, 0, 0)]
self.canvas = canvas.Canvas(
fname, pagesize=(options.paperwidth, options.paperheight))
self.drawDividers(cards)
self.canvas.save()
def add_inline_images(self, text, fontsize):
path = os.path.join(self.options.data_path, 'images')
replace = '<img src='"'%s/coin_small_\\1.png'"' width=%d height='"'100%%'"' valign='"'middle'"'/>' % (
path, fontsize * 1.2)
text = re.sub('(\d)\s(c|C)oin(s)?', replace, text)
replace = '<img src='"'%s/coin_small_question.png'"' width=%d height='"'100%%'"' valign='"'middle'"'/>' % (
path, fontsize * 1.2)
text = re.sub('\?\s(c|C)oin(s)?', replace, text)
replace = '<img src='"'%s/victory_emblem.png'"' width=%d height='"'120%%'"' valign='"'middle'"'/>' % (
path, fontsize * 1.5)
text = re.sub('\<VP\>', 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.options.numDividersHorizontal - 1)
cropmarksleft = (x == 0)
if rightSide:
self.canvas.translate(self.options.dividerWidth, 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.options.dividerWidth, 0)
self.canvas.scale(-1, 1)
if cropmarksleft or cropmarksright:
self.canvas.line(-2 * cmw, 0, -cmw, 0)
self.canvas.line(-2 * cmw,
self.dividerHeight, -cmw, self.dividerHeight)
if y > 0:
self.canvas.line(-2 * cmw,
self.options.dividerHeight, -cmw, self.options.dividerHeight)
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.options.dividerWidth
rightLine = 0
else:
leftLine = 0
rightLine = self.options.dividerWidth
middleLine = self.options.dividerWidth - self.options.labelWidth
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.options.numDividersVertical - 1:
self.canvas.line(rightLine, self.options.dividerHeight + cmw,
rightLine, self.options.dividerHeight + 2 * cmw)
self.canvas.line(middleLine, self.options.dividerHeight + cmw,
middleLine, self.options.dividerHeight + 2 * cmw)
if cropmarksleft:
self.canvas.line(leftLine, self.options.dividerHeight + cmw,
leftLine, self.options.dividerHeight + 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.options.data_path, 'images', 'coin_small.png'),
x, coinHeight, 16, 16, preserveAspectRatio=True, mask='auto')
if card.potcost:
self.canvas.drawImage(os.path.join(self.options.data_path, 'images', 'potion.png'), x + 17,
potHeight, potSize, potSize, preserveAspectRatio=True,
mask=[255, 255, 255, 255, 255, 255])
width += potSize
self.canvas.setFont(self.fontNameBold, 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.options.data_path, 'images', setImage), x, y, 14, 12, mask='auto')
def nameWidth(self, name, fontSize):
w = 0
name_parts = name.split()
for i, part in enumerate(name_parts):
if i != 0:
w += pdfmetrics.stringWidth(' ', self.fontNameRegular, fontSize)
w += pdfmetrics.stringWidth(part[0], self.fontNameRegular, fontSize)
w += pdfmetrics.stringWidth(part[1:],
self.fontNameRegular, 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.options.dividerWidth / 2 - self.options.labelWidth / 2,
self.options.dividerHeight - self.options.labelHeight)
elif not rightSide:
self.canvas.translate(self.options.dividerWidth - self.options.labelWidth,
self.options.dividerHeight - self.options.labelHeight)
else:
self.canvas.translate(0, self.options.dividerHeight - self.options.labelHeight)
# allow for 3 pt border on each side
textWidth = self.options.labelWidth - 6
textHeight = 7
if self.options.no_tab_artwork:
textHeight = 4
textHeight = self.options.labelHeight / 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.options.data_path, 'images', img), 1, 0,
self.options.labelWidth -
2, self.options.labelHeight - 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(self.fontNameOblique, 8)
if setText is None:
setText = ""
self.canvas.drawCentredString(self.options.labelWidth - 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.options.labelWidth - 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.options.labelWidth / 2 - self.nameWidth(line, fontSize) / 2
else:
w = textInset
def drawWordPiece(text, fontSize):
self.canvas.setFont(self.fontNameRegular, fontSize)
if text != ' ':
self.canvas.drawString(w, h, text)
return pdfmetrics.stringWidth(text, self.fontNameRegular, 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.options.labelWidth - textInsetRight - 3
words.reverse()
def drawWordPiece(text, fontSize):
self.canvas.setFont(self.fontNameRegular, fontSize)
if text != ' ':
self.canvas.drawRightString(w, h, text)
return -pdfmetrics.stringWidth(text, self.fontNameRegular, 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, divider_text="card"):
usedHeight = 0
totalHeight = self.options.dividerHeight - self.options.labelHeight
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.options.dividerWidth - 16,
totalHeight - 0.5 * cm - 3)
drewTopIcon = True
if drewTopIcon:
usedHeight += 15
# Figure out what text is to be printed on this divider
if divider_text == "blank":
# blank divider, no need to go on
return
elif divider_text == "rules":
# Add the extra rules text to the divider
if card.extra:
descriptions = (card.extra,)
else:
# Asked for rules and they don't exist, so don't print anything
return
elif divider_text == "card":
# Add the card text to the divider
descriptions = re.split("\n", card.description)
else:
# Don't know what was asked, so don't print anything
return
s = getSampleStyleSheet()['BodyText']
s.fontName = "Times-Roman"
s.alignment = TA_JUSTIFY
textHorizontalMargin = .5 * cm
textVerticalMargin = .3 * cm
textBoxWidth = self.options.dividerWidth - 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, isBack=False, divider_text="card"):
# figure out whether the tab should go on the right side or not
if self.options.tab_side == "right":
rightSide = isBack
elif self.options.tab_side in ["left", "full"]:
rightSide = not isBack
else:
# alternate the cards
if not isBack:
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.options.horizontalMargin, self.options.verticalMargin)
if isBack:
self.canvas.translate(self.options.back_offset, self.options.back_offset_height)
self.canvas.translate(x * self.options.dividerWidthReserved,
y * self.options.dividerHeightReserved)
# actual drawing
if not self.options.tabs_only:
self.drawOutline(
x, y, rightSide, isBack, card.getType().getTypeNames() == ('Expansion',))
self.drawTab(card, rightSide)
if not self.options.tabs_only:
self.drawText(card, divider_text)
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 = self.fontNameRegular
font = pdfmetrics.getFont(fontname)
fontHeightRelative = (font.face.ascent + abs(font.face.descent)) / 1000.0
canFit = False
layouts = [{'rotation': 0,
'minMarginHeight': self.options.minVerticalMargin,
'totalMarginHeight': self.options.verticalMargin,
'width': self.options.paperwidth},
{'rotation': 90,
'minMarginHeight': self.options.minHorizontalMargin,
'totalMarginHeight': self.options.horizontalMargin,
'width': self.options.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.options.numDividersVertical * self.options.numDividersHorizontal)
# 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.options.numDividersHorizontal
y = i / self.options.numDividersHorizontal
self.canvas.saveState()
self.drawDivider(card, x, self.options.numDividersVertical - 1 - y, isBack=False, divider_text=self.options.text_front)
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.text_back == "none":
# Don't print the sheets with the back of the dividers
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.options.numDividersHorizontal - 1 - i) % self.options.numDividersHorizontal
y = i / self.options.numDividersHorizontal
self.canvas.saveState()
self.drawDivider(card, x, self.options.numDividersVertical - 1 - y, isBack=True, divider_text=self.options.text_back)
self.canvas.restoreState()
self.odd = not self.odd
self.canvas.showPage()
if pageNum + 1 == self.options.num_pages:
break