Wendel Voigt 7c4c010fba Refactor of draw.py (#150)
* Major refactor of draw.py
* Also addresses Issue #70, Issue #85, Issue #120, partial to Issue #6
* Make the label information more available so it can be picked up easier by the website frontend
* Add Avery Presta 94211 labels, label names
* Add tab-height to make easier calculations
* height is now the full height of the label.  tab-height is the portion used for the tab.
   (By doing this, adding labels is more intuitive and requires fewer calculations.)
* Fix for issue #239 The fix changes this to now include the original expansion in this case.  So now this results in the original expansion expansion being printed with the upgrade expansion cards included as well.
2019-01-01 20:55:35 -08:00

1713 lines
79 KiB
Python

from __future__ import print_function
import os
import re
import sys
import pkg_resources
from reportlab.lib.units import cm
from reportlab.pdfbase import pdfmetrics
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph, XPreformatted
from reportlab.lib.enums import TA_JUSTIFY, TA_CENTER, TA_LEFT
from reportlab.pdfgen import canvas
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase.pdfmetrics import stringWidth
from .cards import Card
def split(l, n):
i = 0
while i < len(l) - n:
yield l[i:i + n]
i += n
yield l[i:]
class CardPlot(object):
# This object contains information needed to print a divider on a page.
# It goes beyond information about the general card/divider to include page specific drawing information.
# It also includes helpful methods used in manipulating the object and keeping up with tab locations.
LEFT, CENTRE, RIGHT, TOP, BOTTOM = range(100, 105) # location & directional constants
tabNumber = 1 # Number of different tab locations
tabIncrement = 0 # Either 1, 0, or -1. Used to select next tab. This can change if tabSerpentine.
tabIncrementStart = 0 # Starting value of tabIncrement
tabStart = 1 # The starting tab location.
tabStartSide = LEFT # The starting side for the tabs
tabSerpentine = False # What to do at the end of a line of tabs. False = start over. True = reverses direction.
lineType = 'line' # Type of outline to use: line, dot, none
cardWidth = 0 # Width of just the divider, with no extra padding/spacing. NEEDS TO BE SET.
cardHeight = 0 # Height of just the divider, with no extra padding/spacing or tab. NEEDS TO BE SET.
tabWidth = 0 # Width of the tab. NEEDS TO BE SET.
tabHeight = 0 # Height of the tab. NEEDS TO BE SET.
wrapper = False # If the divider is a sleeve/wrapper.
@staticmethod
def tabSetup(tabNumber=None, cardWidth=None, cardHeight=None, tabWidth=None, tabHeight=None,
lineType=None, start=None, serpentine=None, wrapper=None):
# Set up the basic tab information used in calculations when a new CardPlot object is created.
# This needs to be called at least once before the first CardPlot object is created and then it
# needs to be called any time one of the above parameters needs to change.
CardPlot.tabNumber = tabNumber if tabNumber is not None else CardPlot.tabNumber
CardPlot.cardWidth = cardWidth if cardWidth is not None else CardPlot.cardWidth
CardPlot.cardHeight = cardHeight if cardHeight is not None else CardPlot.cardHeight
CardPlot.tabWidth = tabWidth if tabWidth is not None else CardPlot.tabWidth
CardPlot.tabHeight = tabHeight if tabHeight is not None else CardPlot.tabHeight
CardPlot.lineType = lineType if lineType is not None else CardPlot.lineType
CardPlot.tabStartSide = start if start is not None else CardPlot.tabStartSide
CardPlot.tabSerpentine = serpentine if serpentine is not None else CardPlot.tabSerpentine
CardPlot.wrapper = wrapper if wrapper is not None else CardPlot.wrapper
# LEFT tabs RIGHT
# +---+ +---+ +---+ +---+ +---+
# | 1 | | 2 | | 3 | |...| | N | Note: tabNumber = N, N >=1, 0 is for centred tabs
# + +-+ +-+ +-+ +-+ +
# Setup first tab as well as starting point and direction of increment for tabs.
if CardPlot.tabStartSide == CardPlot.RIGHT:
CardPlot.tabStart = CardPlot.tabNumber
CardPlot.tabIncrementStart = -1
elif CardPlot.tabStartSide == CardPlot.CENTRE:
# Get as close to centre as possible
CardPlot.tabStart = (CardPlot.tabNumber + 1) // 2
CardPlot.tabIncrementStart = 1
else:
# LEFT and anything else
CardPlot.tabStartSide = CardPlot.LEFT
CardPlot.tabStart = 1
CardPlot.tabIncrementStart = 1
if CardPlot.tabNumber == 1:
CardPlot.tabIncrementStart = 0
CardPlot.tabIncrement = CardPlot.tabIncrementStart
@staticmethod
def tabRestart():
# Resets the tabIncrement to the starting value and returns the starting tabIndex number.
CardPlot.tabIncrement = CardPlot.tabIncrementStart
return CardPlot.tabStart
def __init__(self, card, x=0, y=0, rotation=0, stackHeight=0, tabIndex=None, page=0,
textTypeFront="card", textTypeBack="rules",
cropOnTop=False, cropOnBottom=False, cropOnLeft=False, cropOnRight=False):
self.card = card
self.x = x # x location of the lower left corner of the card on the page
self.y = y # y location of the lower left corner of the card on the page
self.rotation = rotation # of the card. 0, 90, 180, 270
self.stackHeight = stackHeight # The height of a stack of these cards. Used for interleaving.
self.tabIndex = tabIndex # Tab location index. Starts at 1 and goes up to CardPlot.tabNumber
self.page = page # holds page number of this printed card
self.textTypeFront = textTypeFront # What card text to put on the front of the divider
self.textTypeBack = textTypeBack # What card text to put on the back of the divider
self.cropOnTop = cropOnTop # When true, cropmarks needed along TOP *printed* edge of the card
self.cropOnBottom = cropOnBottom # When true, cropmarks needed along BOTTOM *printed* edge of the card
self.cropOnLeft = cropOnLeft # When true, cropmarks needed along LEFT *printed* edge of the card
self.cropOnRight = cropOnRight # When true, cropmarks needed along RIGHT *printed* edge of the card
# And figure out the backside index
if self.tabIndex == 0:
self.tabIndexBack = 0 # Exact Centre special case, so swapping is still exact centre
elif CardPlot.tabNumber == 1:
self.tabIndex = self.tabIndexBack = 1 # There is only one tab, so can only use 1 for both sides
elif 1 <= self.tabIndex <= CardPlot.tabNumber:
self.tabIndexBack = CardPlot.tabNumber + 1 - self.tabIndex
else:
# For anything else, just start at 1
self.tabIndex = self.tabIndexBack = 1
# Now set the offsets and the closest edge to the tab
if self.tabIndex == 0:
# Special case for centred tabs
self.tabOffset = self.tabOffsetBack = (CardPlot.cardWidth - CardPlot.tabWidth) / 2
self.closestSide = CardPlot.CENTRE
elif CardPlot.tabNumber <= 1:
# If just one tab, then can be right, centre, or left
self.closestSide = CardPlot.tabStartSide
if CardPlot.tabStartSide == CardPlot.RIGHT:
self.tabOffset = CardPlot.cardWidth - CardPlot.tabWidth
self.tabOffsetBack = 0
elif CardPlot.tabStartSide == CardPlot.CENTRE:
self.tabOffset = (CardPlot.cardWidth - CardPlot.tabWidth) / 2
self.tabOffsetBack = (CardPlot.cardWidth - CardPlot.tabWidth) / 2
else:
# LEFT and anything else
self.tabOffset = 0
self.tabOffsetBack = CardPlot.cardWidth - CardPlot.tabWidth
else:
# More than 1 tabs
self.tabOffset = (self.tabIndex - 1) * (
(CardPlot.cardWidth - CardPlot.tabWidth) / (CardPlot.tabNumber - 1))
self.tabOffsetBack = CardPlot.cardWidth - CardPlot.tabWidth - self.tabOffset
# Set which edge is closest to the tab
if self.tabIndex <= CardPlot.tabNumber / 2:
self.closestSide = CardPlot.LEFT
else:
self.closestSide = CardPlot.RIGHT if self.tabIndex > (CardPlot.tabNumber + 1) / 2 else CardPlot.CENTRE
def setXY(self, x, y, rotation=None):
# set the card to the given x,y and optional rotation
self.x = x
self.y = y
if rotation is not None:
self.rotation = rotation
def rotate(self, delta):
# rotate the card by amount delta
self.rotation = (self.rotation + delta) % 360
def getTabOffset(self, backside=False):
# Get the tab offset (from the left edge) of the tab given
if backside:
return self.tabOffsetBack
else:
return self.tabOffset
def nextTab(self, tab=None):
# For a given tab, calculate the next tab in the sequence
tab = tab if tab is not None else self.tabIndex
if CardPlot.tabNumber == 1:
return 1 # it is the same, nothing else to do
# Increment if in range
if 1 <= tab <= CardPlot.tabNumber:
tab += CardPlot.tabIncrement
# Now check for wrap around
if tab > CardPlot.tabNumber:
tab = 1
elif tab < 1:
tab = CardPlot.tabNumber
if CardPlot.tabSerpentine and CardPlot.tabNumber > 2:
if (tab == 1) or (tab == CardPlot.tabNumber):
# reverse direction for next tab
CardPlot.tabIncrement *= -1
return tab
def getClosestSide(self, backside=False):
# Get the closest side for this tab.
# Used when wanting text to be aligned towards the outer edge.
side = self.closestSide
if backside:
# Need to flip
if side == CardPlot.LEFT:
side = CardPlot.RIGHT
elif side == CardPlot.RIGHT:
side = CardPlot.LEFT
return side
def flipFront2Back(self):
# Flip a card from front to back. i.e., print the front of the divider on the page's back
# and print the back of the divider on the page's front. So what does that mean...
# If it is a wrapper / slipcover, then it is rotated 180 degrees.
# Otherwise, the tab moves from right(left) to left(right). If centre, it stays the same.
# And then the divider's text is moved to the other side of the page.
if self.wrapper:
self.rotate(180)
else:
self.tabIndex, self.tabIndexBack = self.tabIndexBack, self.tabIndex
self.tabOffset, self.tabOffsetBack = self.tabOffsetBack, self.tabOffset
self.textTypeFront, self.textTypeBack = self.textTypeBack, self.textTypeFront
self.closestSide = self.getClosestSide(backside=True)
def translate(self, canvas, page_width, backside=False):
# Translate the page x,y of the lower left of item, taking into account the rotation,
# and set up the canvas so that (0,0) is now at the lower lower left of the item
# and the item can be drawn as if it is in the "standard" orientation.
# So when done, the canvas is set and ready to draw the divider
x = self.x
y = self.y
rotation = self.rotation
# set width and height for this card
width = self.cardWidth
height = self.cardHeight + self.tabHeight
if self.wrapper:
height = 2 * (height + self.stackHeight)
if backside:
x = page_width - x - width
if self.rotation == 180:
x += width
y += height
elif self.rotation == 90:
if backside:
x += width
rotation = 270
else:
y += width
elif self.rotation == 270:
if backside:
x += width - height
y += width
rotation = 90
else:
x += height
rotation = 360 - rotation % 360 # ReportLab rotates counter clockwise, not clockwise.
canvas.translate(x, y)
canvas.rotate(rotation)
def translateCropmarkEnable(self, side):
# Returns True if a cropmark is needed on that side of the card
# Takes into account the card's rotation, if the tab is flipped, if the card is next to an edge, etc.
# First the rotation. The page does not change even if the card is rotated.
# So need to translate page side to the actual drawn card edge
if self.rotation == 0:
sideTop = self.cropOnTop
sideBottom = self.cropOnBottom
sideRight = self.cropOnRight
sideLeft = self.cropOnLeft
elif self.rotation == 90:
sideTop = self.cropOnRight
sideBottom = self.cropOnLeft
sideRight = self.cropOnBottom
sideLeft = self.cropOnTop
elif self.rotation == 180:
sideTop = self.cropOnBottom
sideBottom = self.cropOnTop
sideRight = self.cropOnLeft
sideLeft = self.cropOnRight
elif self.rotation == 270:
sideTop = self.cropOnLeft
sideBottom = self.cropOnRight
sideRight = self.cropOnTop
sideLeft = self.cropOnBottom
# Now can return the proper value based upon what side is requested
if side == self.TOP:
return sideTop
elif side == self.BOTTOM:
return sideBottom
elif side == self.RIGHT:
return sideRight
elif side == self.LEFT:
return sideLeft
else:
return False # just in case
class Plotter(object):
# Creates a simple plotting object that goes from point to point.
# This makes outline drawing easier since calculations only need to be the delta from
# one point to the next. The default plotting in reportlab requires both
# ends of the line in absolute sense. Thus calculations can become increasingly more
# complicated given various options. Using this object simplifies the calculations significantly.
def __init__(self, canvas, x=0, y=0, cropmarkLength=-1, cropmarkSpacing=-1):
self.canvas = canvas
self.x = x
self.y = y
self.LEFT, self.RIGHT, self.TOP, self.BOTTOM, self.LINE, self.NO_LINE, self.DOT = range(1, 8) # Constants
if cropmarkLength < 0:
cropmarkLength = 0.2
if cropmarkSpacing < 0:
cropmarkSpacing = 0.1
self.CropMarkLength = cropmarkLength * cm # The length of a cropmark
self.CropMarkSpacing = cropmarkSpacing * cm # The spacing between the cut point and the start of the cropmark
self.DotSize = 0.2 # Size of dot marks
self.CropEnable = {self.LEFT: False, self.RIGHT: False, self.TOP: False, self.BOTTOM: False}
def setXY(self, x, y):
self.x = x
self.y = y
def getXY(self):
return (self.x, self.y)
def setCropEnable(self, mark, enable=False):
if mark in self.CropEnable:
self.CropEnable[mark] = enable
def plot(self, delta_x=0, delta_y=0, pen=False, cropmarks=[]):
# Move the pen, drawing along the way
if pen is False:
pen = self.NO_LINE
x, y = self.getXY() # get current point
new_x = x + delta_x # calculate new point from delta
new_y = y + delta_y
if pen == self.LINE:
self.canvas.line(x, y, new_x, new_y)
if pen == self.DOT:
self.canvas.circle(new_x, new_y, self.DotSize)
self.setXY(new_x, new_y) # save the new point
# Make sure cropmarks is a list
cropmarks = cropmarks if isinstance(cropmarks, list) else [cropmarks]
# Now add any cropmarks
for mark in cropmarks:
# setCropEnable must be called for each direction ahead of time (once per divider).
# Cropmarks are only drawn for directions that are enabled (as set above).
# Each crop mark given is either:
# 1. A tuple of direction and a boolean of additional enablement criteria
# 2. A direction to draw a drop mark
if isinstance(mark, tuple):
direction, enable = mark
enable = enable and self.CropEnable[direction] if direction in self.CropEnable else False
else:
direction = mark
enable = self.CropEnable[direction] if direction in self.CropEnable else False
if direction in self.CropEnable:
self.cropmark(direction, enable)
def cropmark(self, direction, enabled=False):
# From current point, draw a cropmark in the correct direction and return to starting point
if enabled:
x, y = self.getXY() # Saving for later
if direction == self.TOP:
self.plot(0, self.CropMarkSpacing)
self.plot(0, self.CropMarkLength, self.LINE)
if direction == self.BOTTOM:
self.plot(0, -self.CropMarkSpacing)
self.plot(0, -self.CropMarkLength, self.LINE)
if direction == self.RIGHT:
self.plot(self.CropMarkSpacing, 0)
self.plot(self.CropMarkLength, 0, self.LINE)
if direction == self.LEFT:
self.plot(-self.CropMarkSpacing, 0)
self.plot(-self.CropMarkLength, 0, self.LINE)
self.setXY(x, y) # Restore to starting point
class DividerDrawer(object):
def __init__(self, options=None):
self.canvas = None
self.pages = None
self.options = options
@staticmethod
def get_image_filepath(fname):
return pkg_resources.resource_filename('domdiv', os.path.join('images', fname))
def draw(self, cards=[], options=None):
if options is not None:
self.options = options
self.registerFonts()
self.canvas = canvas.Canvas(
self.options.outfile,
pagesize=(self.options.paperwidth, self.options.paperheight))
self.drawDividers(cards)
if self.options.info or self.options.info_all:
self.drawInfo()
self.canvas.save()
def registerFonts(self):
# the following are filenames from both an Adobe Reader install and a download from fontsgeek
fontfilenames = ['MinionPro-Regular.ttf',
'MinionPro-Bold.ttf',
'MinionPro-It.ttf',
'Minion Pro Regular.ttf',
'Minion Pro Bold.ttf',
'Minion Pro Italic.ttf']
# first figure out which, if any, are present
fontpaths = [os.path.join('fonts', fname) for fname in fontfilenames]
fontpaths = [fpath for fpath in fontpaths if pkg_resources.resource_exists('domdiv', fpath)]
self.font_mapping = {'Regular': [fpath for fpath in fontpaths if 'Regular' in fpath],
'Bold': [fpath for fpath in fontpaths if 'Bold' in fpath],
'Italic': [fpath for fpath in fontpaths if 'It' in fpath]}
# then make sure that we have at least one for each type
for fonttype in self.font_mapping:
if not len(self.font_mapping[fonttype]):
print(("Warning, Minion Pro ttf file for {} missing from domdiv/fonts!"
" Falling back on Times font for everything.").format(fonttype), file=sys.stderr)
self.font_mapping = {'Regular': 'Times-Roman',
'Bold': 'Times-Bold',
'Italic': 'Times-Oblique'}
break
else:
# and finally register and tag one for each type
ftag = 'MinionPro-{}'.format(fonttype)
pdfmetrics.registerFont(TTFont(ftag,
pkg_resources.resource_filename('domdiv',
self.font_mapping[fonttype][0])))
self.font_mapping[fonttype] = ftag
self.font_mapping['Monospaced'] = 'Courier'
def drawTextPages(self, pages, margin=1.0, fontsize=10, leading=10, spacer=0.05):
s = getSampleStyleSheet()['BodyText']
s.fontName = self.font_mapping['Monospaced']
s.alignment = TA_LEFT
textHorizontalMargin = margin * cm
textVerticalMargin = margin * cm
textBoxWidth = self.options.paperwidth - 2 * textHorizontalMargin
textBoxHeight = self.options.paperheight - 2 * textVerticalMargin
minSpacerHeight = 0.05 * cm
for page in pages:
s.fontsize = fontsize
s.leading = leading
spacerHeight = spacer * cm
text = re.split("\n", page)
while True:
paragraphs = []
# this accounts for the spacers we insert between paragraphs
h = (len(text) - 1) * spacerHeight
for line in text:
p = XPreformatted(line, 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 -= 0.2
s.leading -= 0.2
spacerHeight = max(spacerHeight - 1, minSpacerHeight)
h = self.options.paperheight - textVerticalMargin
for p in paragraphs:
h -= p.height
p.drawOn(self.canvas, textHorizontalMargin, h)
h -= spacerHeight
self.canvas.showPage()
def drawInfo(self, printIt=True):
# Keep track of the number of pages
pageCount = 0
# A unique separator that will not be found in any normal text. Was '@@@***!!!***@@@' at one time.
sep = chr(30) + chr(31)
# Generic space. Other options are ' ', '&nbsp;', '&#xa0;'
space = '&nbsp;'
tab_spaces = 4
blank_line = (space + '\n') * 2
if self.options.info or self.options.info_all:
text = "<para alignment='center'><font size=18><b>"
text += "Sumpfork's Dominion Tabbed Divider Generator"
text += "</b></font></para>\n"
text += blank_line
text += "Online generator at: "
text += "<a href='http://domtabs.sandflea.org/' color='blue'>http://domtabs.sandflea.org</a>\n\n"
text += "Source code on GitHub at: "
text += "<a href='https://github.com/sumpfork/dominiontabs' color='blue'>"
text += "https://github.com/sumpfork/dominiontabs</a>\n\n"
text += "Options for this file:\n"
cmd = " ".join(self.options.argv)
cmd = cmd.replace(' --', sep + '--')
cmd = cmd.replace(' -', sep + '-')
cmd = cmd.replace(sep, '\n' + space * tab_spaces)
text += cmd
text += blank_line
if printIt:
self.drawTextPages([text], margin=1.0, fontsize=10, leading=10, spacer=0.05)
pageCount += 1
if self.options.info_all:
linesPerPage = 80
lines = self.options.help.replace('\n\n', blank_line).replace(' ', space).split('\n')
pages = []
lineCount = 0
text = ""
for line in lines:
lineCount += 1
text += line + '\n'
if lineCount >= linesPerPage:
pages.append(text)
pageCount += 1
lineCount = 0
text = ""
if text:
pages.append(text)
pageCount += 1
if printIt:
self.drawTextPages(pages, margin=0.75, fontsize=6, leading=7, spacer=0.1)
return pageCount
def wantCentreTab(self, card):
return (card.isExpansion() and self.options.centre_expansion_dividers) or self.options.tab_side == "centre"
def drawOutline(self, item, isBack=False):
# draw outline or cropmarks
if isBack and not self.options.cropmarks:
return
if self.options.linewidth <= 0.0:
return
self.canvas.saveState()
self.canvas.setLineWidth(self.options.linewidth)
# The back is flipped
if isBack:
self.canvas.translate(item.cardWidth, 0)
self.canvas.scale(-1, 1)
plotter = Plotter(self.canvas,
cropmarkLength=self.options.cropmarkLength,
cropmarkSpacing=self.options.cropmarkSpacing)
dividerWidth = item.cardWidth
dividerHeight = item.cardHeight + item.tabHeight
dividerBaseHeight = item.cardHeight
tabLabelWidth = item.tabWidth
theTabWidth = item.tabWidth
theTabHeight = item.tabHeight
left2tab = item.getTabOffset(backside=isBack)
right2tab = dividerWidth - tabLabelWidth - left2tab
nearZero = 0.01
left2tab = left2tab if left2tab > nearZero else 0
right2tab = right2tab if right2tab > nearZero else 0
if item.lineType.lower() == 'line':
lineType = plotter.LINE
lineTypeNoDot = plotter.LINE
elif item.lineType.lower() == 'dot':
lineType = plotter.DOT
lineTypeNoDot = plotter.NO_LINE
else:
lineType = plotter.NO_LINE
lineTypeNoDot = plotter.NO_LINE
# Setup bare minimum lineStyle's
lineStyle = [lineType for i in range(0, 10)]
lineStyle[0] = lineTypeNoDot
lineStyle[7] = lineType
lineStyle[8] = lineType if left2tab > 0 else lineTypeNoDot
lineStyle[9] = lineType if right2tab > 0 else lineTypeNoDot
RIGHT = plotter.RIGHT
LEFT = plotter.LEFT
BOTTOM = plotter.BOTTOM
TOP = plotter.TOP
NO_LINE = plotter.NO_LINE
plotter.setCropEnable(RIGHT, self.options.cropmarks and item.translateCropmarkEnable(item.RIGHT))
plotter.setCropEnable(LEFT, self.options.cropmarks and item.translateCropmarkEnable(item.LEFT))
plotter.setCropEnable(TOP, self.options.cropmarks and item.translateCropmarkEnable(item.TOP))
plotter.setCropEnable(BOTTOM, self.options.cropmarks and item.translateCropmarkEnable(item.BOTTOM))
if not item.wrapper:
# Normal Card Outline
# <-left2tab-> <--tabLabelWidth--> <-right2tab->
# | | | |
# Z-+ F7-------------------7E +-Y
# | |
# H-8------------8 9-------------9-C
# | G D |
# | Generic Divider |
# | Tab Centered or to the Side |
# | |
# A-7------------0-------------------0-------------7-B
# | V| W| |
#
plotter.plot(0, 0, NO_LINE, [LEFT, BOTTOM]) # ? to A
plotter.plot(left2tab, 0, lineStyle[0], BOTTOM) # A to V
plotter.plot(theTabWidth, 0, lineStyle[0], BOTTOM) # V to W
plotter.plot(right2tab, 0, lineStyle[7], [BOTTOM, RIGHT]) # W to B
plotter.plot(0, dividerBaseHeight, lineStyle[9], RIGHT) # B to C
plotter.plot(-right2tab, 0, lineStyle[9]) # C to D
plotter.plot(0, theTabHeight, lineStyle[7], TOP) # D to E
plotter.plot(right2tab, 0, NO_LINE, [TOP, RIGHT]) # E to Y
plotter.plot(-right2tab, 0, NO_LINE) # Y to E
plotter.plot(-theTabWidth, 0, lineStyle[7], TOP) # E to F
plotter.plot(0, -theTabHeight, lineStyle[8]) # F to G
plotter.plot(-left2tab, 0, lineStyle[8], LEFT) # G to H
plotter.plot(0, theTabHeight, NO_LINE, [TOP, LEFT]) # H to Z
plotter.plot(0, -theTabHeight, NO_LINE) # Z to H
plotter.plot(0, -dividerBaseHeight, lineStyle[7]) # H to A
else:
# Card Wrapper Outline
# Set up values used in the outline
minNotch = 0.1 * cm # Don't really want notches that are smaller than this.
if self.options.notch_length * cm > minNotch:
# A notch length was given, so notches are wanted
notch_height = self.options.notch_height * cm # thumb notch height
notch1 = notch2 = notch3 = notch4 = self.options.notch_length * cm # thumb notch width
notch1used = notch2used = notch3used = notch4used = True # For now
else:
# No notches are wanted
notch_height = 0
notch1 = notch2 = notch3 = notch4 = 0
notch1used = notch2used = notch3used = notch4used = False
# Even if wanted, there may not be room, and limit to one pair of notches
if (right2tab - minNotch < notch1) or not notch1used:
notch1 = 0
notch1used = False
if (left2tab - minNotch < notch4) or not notch4used or notch1used:
notch4 = notch2 = 0
notch4used = notch2used = False
else:
notch3 = 0
notch3used = False
# Setup the rest of the lineStyle's
lineStyle[1] = lineType if notch1used else lineTypeNoDot
lineStyle[2] = lineType if notch2used else lineTypeNoDot
lineStyle[3] = lineType if notch3used else lineTypeNoDot
lineStyle[4] = lineType if notch4used else lineTypeNoDot
lineStyle[5] = lineType if notch1used and right2tab > 0 else lineTypeNoDot
lineStyle[6] = lineType if notch4used and left2tab > 0 else lineTypeNoDot
stackHeight = item.stackHeight
body_minus_notches = dividerBaseHeight - (2.0 * notch_height)
tab2notch1 = right2tab - notch1
tab2notch4 = left2tab - notch4
# <-----left2tab----------> <--tabLabelWidth--> <-----right2tab-------->
# | | | | | |
# Zb-+ Va+ V7-------------------7U +Ua +-Ub
# <--tab2notch4->| |<--tab2notch1->
# + W0...................0T
# Y | | R
# Za-+ 8---------------8...................9---------------9 +-Pa
# <notch4 >| X S |<notch1>
# Z-6---------4Ya Q1--------5-P
# | |
# | Generic Wrapper |
# | Normal Side |
# | |
# AA-2--------2BB N3--------3-O
# <notch2>| |<notch3>
# + 0CC.................................................M0 +
# | |
# + 0DD.................................................L0 +
# <notch2>| |<notch3>
# FF-2--------2EE K3--------3-J
# | |
# | Reverse Side |
# | rotated 180 |
# | Ca H |
# GG-6---------4<--tab2notch4-> <--tab2notch1->1--------5-I
# <notch4 >| C F |<notch1>
# B-+ Cb8---------------8 9---------------1G +-Ia
# | |
# -+A Cc+ D7-------------------7E +Ga +-Ib
# | | | | | |
# <-----left2tab----------> <--tabLabelWidth--> <-----right2tab-------->
plotter.plot(0, 0, NO_LINE, [BOTTOM, LEFT]) # ? to A
plotter.plot(0, theTabHeight, NO_LINE, LEFT) # A to B
plotter.plot(0, notch_height, NO_LINE, (LEFT, notch4used or notch1used)) # B to GG
plotter.plot(notch4, 0, lineStyle[4]) # GG to Ca
plotter.plot(0, -notch_height, lineStyle[8]) # Ca to Cb
plotter.plot(0, -theTabHeight, NO_LINE, (BOTTOM, notch4used or notch2used)) # Cb to Cc
plotter.plot(0, theTabHeight, NO_LINE) # Cc to Cb
plotter.plot(tab2notch4, 0, lineStyle[8]) # Cb to C
plotter.plot(0, -theTabHeight, lineStyle[7], BOTTOM) # C to D
plotter.plot(tabLabelWidth, 0, lineStyle[7], BOTTOM) # D to E
plotter.plot(0, theTabHeight, lineStyle[9]) # E to F
plotter.plot(tab2notch1, 0, lineStyle[1]) # F to G
plotter.plot(0, -theTabHeight, NO_LINE, (BOTTOM, notch1used or notch3used)) # G to Ga
plotter.plot(0, theTabHeight, NO_LINE) # Ga to G
plotter.plot(0, notch_height, lineStyle[1]) # G to H
plotter.plot(notch1, 0, lineStyle[5], (RIGHT, notch1used or notch4used)) # H to I
plotter.plot(0, -notch_height, NO_LINE, RIGHT) # I to Ia
plotter.plot(0, -theTabHeight, NO_LINE, [RIGHT, BOTTOM]) # Ia to Ib
plotter.plot(0, theTabHeight, NO_LINE) # Ib to Ia
plotter.plot(0, notch_height, NO_LINE) # Ia to I
plotter.plot(0, body_minus_notches, lineStyle[3], (RIGHT, notch2used or notch3used)) # I to J
plotter.plot(-notch3, 0, lineStyle[3]) # J to K
plotter.plot(0, notch_height, lineStyle[0]) # K to L
plotter.plot(0, stackHeight, lineStyle[0]) # L to M
plotter.plot(0, notch_height, lineStyle[3]) # M to N
plotter.plot(notch3, 0, lineStyle[3], (RIGHT, notch2used or notch3used)) # N to O
plotter.plot(0, body_minus_notches, lineStyle[5], (RIGHT, notch1used or notch4used)) # O to P
plotter.plot(0, notch_height, NO_LINE, RIGHT) # P to Pa
plotter.plot(0, -notch_height, NO_LINE) # Pa to P
plotter.plot(-notch1, 0, lineStyle[1]) # P to Q
plotter.plot(0, notch_height, lineStyle[9]) # Q to R
plotter.plot(-tab2notch1, 0, lineStyle[9]) # R to S
plotter.plot(0, stackHeight, lineStyle[0]) # S to T
plotter.plot(0, theTabHeight, lineStyle[7], TOP) # S to U
plotter.plot(tab2notch1, 0, NO_LINE, (TOP, notch1used or notch3used)) # U to Ua
plotter.plot(notch1, 0, NO_LINE, [TOP, RIGHT]) # Ua to Ub
plotter.plot(-notch1, 0, NO_LINE) # Ub to Ua
plotter.plot(-tab2notch1, 0, NO_LINE) # Ua to U
plotter.plot(-theTabWidth, 0, lineStyle[7], TOP) # U to V
plotter.plot(-tab2notch4, 0, NO_LINE, (TOP, notch4used or notch2used)) # V to Va
plotter.plot(tab2notch4, 0, NO_LINE) # Va to V
plotter.plot(0, -theTabHeight, lineStyle[0]) # V to W
plotter.plot(0, -stackHeight, lineStyle[8]) # W to X
plotter.plot(-tab2notch4, 0, lineStyle[8]) # X to Y
plotter.plot(0, -notch_height, lineStyle[4]) # Y to Ya
plotter.plot(-notch4, 0, lineStyle[6], (LEFT, notch1used or notch4used)) # Ya to Z
plotter.plot(0, notch_height, NO_LINE, LEFT) # Z to Za
plotter.plot(0, theTabHeight + stackHeight, NO_LINE, [TOP, LEFT]) # Za to Zb
plotter.plot(0, -theTabHeight - stackHeight, NO_LINE) # Zb to Za
plotter.plot(0, -notch_height, NO_LINE) # Za to Z
plotter.plot(0, -body_minus_notches, lineStyle[2], (LEFT, notch2used or notch3used)) # Z to AA
plotter.plot(notch2, 0, lineStyle[2]) # AA to BB
plotter.plot(0, -notch_height, lineStyle[0]) # BB to CC
plotter.plot(0, -stackHeight, lineStyle[0]) # CC to DD
plotter.plot(0, -notch_height, lineStyle[2]) # DD to EE
plotter.plot(-notch2, 0, lineStyle[2], (LEFT, notch2used or notch3used)) # EE to FF
plotter.plot(0, -body_minus_notches, lineStyle[6]) # FF to GG
# Add fold lines
self.canvas.setStrokeGray(0.9)
plotter.setXY(left2tab, dividerHeight + stackHeight + dividerBaseHeight) # ? to X
plotter.plot(theTabWidth, 0, plotter.LINE) # X to S
plotter.plot(0, stackHeight) # S to T
plotter.plot(-theTabWidth, 0, plotter.LINE) # V to S
plotter.setXY(notch2, dividerHeight) # ? to DD
plotter.plot(dividerWidth - notch2 - notch3, 0, plotter.LINE) # DD to L
plotter.plot(0, stackHeight) # L to M
plotter.plot(-dividerWidth + notch2 + notch3, 0, plotter.LINE) # M to CC
self.canvas.restoreState()
def add_inline_images(self, text, fontsize):
def replace_image_tag(text,
fontsize,
tag_pattern,
fname_replace,
fontsize_multiplier,
height_percent,
text_fontsize_multiplier=None):
replace_template = '<img src="{fpath}" width={width} height="{height_percent}%" valign="middle" />'
offset = 0
for match in re.finditer(tag_pattern, text):
replace = replace_template
tag = match.group(0)
fname = re.sub(tag_pattern, fname_replace, tag)
if text_fontsize_multiplier is not None:
font_replace = re.sub(tag_pattern,
'<font size={}>\\1</font>'.format(fontsize * text_fontsize_multiplier),
tag)
replace = font_replace + replace
replace = replace.format(fpath=DividerDrawer.get_image_filepath(fname),
width=fontsize * fontsize_multiplier,
height_percent=height_percent)
text = text[:match.start() + offset] + replace + text[match.end() + offset:]
offset += len(replace) - len(match.group(0))
return text
# Coins
replace_specs = [
# Coins
(r'(\d+)\s\<\*COIN\*\>', 'coin_small_\\1.png', 2.4, 200),
(r'(\d+)\s(c|C)oin(s)?', 'coin_small_\\1.png', 1.2, 100),
(r'\?\s(c|C)oin(s)?', 'coin_small_question.png', 1.2, 100),
(r'(empty|\_)\s(c|C)oin(s)?', 'coin_small_empty.png', 1.2, 100),
# VP
(r'(?:\s+|\<)VP(?:\s+|\>|\.|$)', 'victory_emblem.png', 1.25, 100),
(r'(\d+)\s*\<\*VP\*\>', 'victory_emblem.png', 2, 160, 1.3),
# Debt
(r'(\d+)\sDebt', 'debt_\\1.png', 1.2, 105),
(r'Debt', 'debt.png', 1.2, 105),
# Potion
(r'(\d+)\s*\<\*POTION\*\>', 'potion_small.png', 2, 140, 1.5),
(r'Potion', 'potion_small.png', 1.2, 100)
]
for args in replace_specs:
text = replace_image_tag(text, fontsize, *args)
return text.strip()
def add_inline_text(self, card, text):
# Bonuses
text = card.getBonusBoldText(text)
# <line>
replace = "<center>{}</center>\n".format("&ndash;" * 22)
text = re.sub(r"\<line\>", replace, text)
# <tab> and \t
text = re.sub(r"\<tab\>", '\t', text)
text = re.sub(r"\<t\>", '\t', text)
text = re.sub(r"\t", "&nbsp;" * 4, text)
# various breaks
text = re.sub(r"\<br\>", "<br />", text)
text = re.sub(r"\<n\>", "\n", text)
# alignments
text = re.sub(r"\<c\>", "<center>", text)
text = re.sub(r"\<center\>", "\n<para alignment='center'>", text)
text = re.sub(r"\</c\>", "</center>", text)
text = re.sub(r"\</center\>", "</para>", text)
text = re.sub(r"\<l\>", "<left>", text)
text = re.sub(r"\<left\>", "\n<para alignment='left'>", text)
text = re.sub(r"\</l\>", "</left>", text)
text = re.sub(r"\</left\>", "</para>", text)
text = re.sub(r"\<r\>", "<right>", text)
text = re.sub(r"\<right\>", "\n<para alignment='right'>", text)
text = re.sub(r"\</r\>", "</right>", text)
text = re.sub(r"\</right\>", "</para>", text)
text = re.sub(r"\<j\>", "<justify>", text)
text = re.sub(r"\<justify\>", "\n<para alignment='justify'>", text)
text = re.sub(r"\</j\>", "</justify>", text)
text = re.sub(r"\</justify\>", "</para>", text)
return text.strip().strip('\n')
def drawCardCount(self, card, x, y, offset=-1):
# Note that this is right justified.
# x represents the right most for the image (image grows to the left)
if card.getCardCount() < 1:
return 0
# draw_list = [(card.getCardCount(), 1)]
draw_list = sorted([(i, card.count.count(i)) for i in set(card.count)])
cardIconHeight = y + offset
countHeight = cardIconHeight - 4
width = 0
for value, count in draw_list:
# draw the image set with the number of cards inside it
width += 16
x -= 16
self.canvas.drawImage(
DividerDrawer.get_image_filepath('card.png'),
x,
countHeight,
16,
16,
preserveAspectRatio=True,
mask='auto')
self.canvas.setFont(self.font_mapping['Bold'], 10)
self.canvas.drawCentredString(x + 8, countHeight + 4, str(value))
# now draw the number of sets
if count > 1:
count_string = u"{}\u00d7".format(count)
width_string = stringWidth(count_string, self.font_mapping['Regular'], 10)
width_string -= 1 # adjust to make it closer to image
width += width_string
x -= width_string
self.canvas.setFont(self.font_mapping['Regular'], 10)
self.canvas.drawString(x, countHeight + 4, count_string)
return width + 1
def drawCost(self, card, x, y, costOffset=-1):
# width starts at 2 (1 pt border on each side)
width = 2
costHeight = y + costOffset
coinHeight = costHeight - 5
potHeight = y - 3
potSize = 11
if (not(card.cost == "" or
(card.debtcost and int(card.cost) == 0) or
(card.potcost and int(card.cost) == 0))):
self.canvas.drawImage(
DividerDrawer.get_image_filepath('coin_small.png'),
x,
coinHeight,
16,
16,
preserveAspectRatio=True,
mask='auto')
self.canvas.setFont(self.font_mapping['Bold'], 12)
self.canvas.drawCentredString(x + 8, costHeight, str(card.cost))
self.canvas.setFillColorRGB(0, 0, 0)
x += 17
width += 16
if card.debtcost:
self.canvas.drawImage(
DividerDrawer.get_image_filepath('debt.png'),
x,
coinHeight,
16,
16,
preserveAspectRatio=True,
mask=[170, 255, 170, 255, 170, 255])
self.canvas.setFillColorRGB(1, 1, 1)
self.canvas.setFont(self.font_mapping['Bold'], 12)
self.canvas.drawCentredString(x + 8, costHeight, str(card.debtcost))
self.canvas.setFillColorRGB(0, 0, 0)
x += 17
width += 16
if card.potcost:
self.canvas.drawImage(
DividerDrawer.get_image_filepath('potion.png'),
x,
potHeight,
potSize,
potSize,
preserveAspectRatio=True,
mask='auto')
width += potSize
return width
def drawSetIcon(self, setImage, x, y):
# set image
w = 2
self.canvas.drawImage(
DividerDrawer.get_image_filepath(setImage),
x,
y,
14,
12,
mask='auto')
return w + 14
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.font_mapping['Regular'],
fontSize)
w += pdfmetrics.stringWidth(part[0], self.font_mapping['Regular'],
fontSize)
w += pdfmetrics.stringWidth(part[1:], self.font_mapping['Regular'],
fontSize - 2)
return w
def drawTab(self, item, wrapper="no", backside=False):
card = item.card
# Skip blank cards
if card.isBlank():
return
# draw tab flap
self.canvas.saveState()
translate_y = item.cardHeight
if self.wantCentreTab(card):
translate_x = item.cardWidth / 2 - item.tabWidth / 2
else:
translate_x = item.getTabOffset(backside=backside)
if wrapper == "back":
translate_y = item.tabHeight
if self.wantCentreTab(card):
translate_x = item.cardWidth / 2 + item.tabWidth / 2
else:
translate_x = item.getTabOffset(backside=False) + item.tabWidth
if wrapper == "front":
translate_y = translate_y + item.cardHeight + item.tabHeight + 2.0 * item.stackHeight
self.canvas.translate(translate_x, translate_y)
if wrapper == "back":
self.canvas.rotate(180)
if self.options.black_tabs:
self.canvas.saveState()
self.canvas.setFillColorRGB(0, 0, 0)
self.canvas.rect(0, 0, item.tabWidth, item.tabHeight, fill=True)
self.canvas.restoreState()
# allow for 3 pt border on each side
textWidth = item.tabWidth - 6
textHeight = 7
if self.options.no_tab_artwork:
textHeight = 4
textHeight = item.tabHeight / 2 - textHeight + \
card.getType().getTabTextHeightOffset()
# draw banner
img = card.getType().getTabImageFile()
if not self.options.no_tab_artwork and img:
self.canvas.drawImage(
DividerDrawer.get_image_filepath(img),
1,
0,
item.tabWidth - 2,
item.tabHeight - 1,
preserveAspectRatio=False,
anchor='n',
mask='auto')
# draw cost
if not card.isExpansion() and not card.isBlank(
) and not card.isLandmark() and not card.isType('Trash'):
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.font_mapping['Italic'], 8)
if setText is None:
setText = ""
self.canvas.drawCentredString(item.tabWidth - 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, item.tabWidth - 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
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]
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()
NotRightEdge = (
not self.options.tab_name_align == "right" and
(self.options.tab_name_align == "centre" or
item.getClosestSide(backside=backside) != CardPlot.RIGHT or
not self.options.tab_name_align == "edge"))
if wrapper == "back" and not self.options.tab_name_align == "centre":
NotRightEdge = not NotRightEdge
if NotRightEdge:
if (self.options.tab_name_align == "centre" or self.wantCentreTab(card)
or (item.getClosestSide(backside=backside) == CardPlot.CENTRE)):
w = item.tabWidth / 2 - self.nameWidth(line, fontSize) / 2
else:
w = textInset
def drawWordPiece(text, fontSize):
self.canvas.setFont(self.font_mapping['Regular'], fontSize)
if text != ' ':
self.canvas.drawString(w, h, text)
return pdfmetrics.stringWidth(text, self.font_mapping['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
if self.options.tab_name_align == "centre" or self.wantCentreTab(card):
w = item.tabWidth / 2 - self.nameWidth(line, fontSize) / 2
w = item.tabWidth - w
else:
w = item.tabWidth - textInsetRight
# to make tabs easier to read when grouped together extra 3pt is for
# space between text + set symbol
w -= 3
words.reverse()
def drawWordPiece(text, fontSize):
self.canvas.setFont(self.font_mapping['Regular'], fontSize)
if text != ' ':
self.canvas.drawRightString(w, h, text)
return -pdfmetrics.stringWidth(text, self.font_mapping['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)
if wrapper == "front" and card.getCardCount() >= 5:
# Print smaller version of name on the top wrapper edge
self.canvas.translate(0, -item.stackHeight) # move into area used by the wrapper
fontSize = 8 # use the smallest font
self.canvas.setFont(self.font_mapping['Regular'], fontSize)
textHeight = fontSize - 2
textHeight = item.stackHeight / 2 - textHeight / 2
h = textHeight
words = name.split()
w = item.tabWidth / 2 - self.nameWidth(name, fontSize) / 2
def drawWordPiece(text, fontSize):
self.canvas.setFont(self.font_mapping['Regular'], fontSize)
if text != ' ':
self.canvas.drawString(w, h, text)
return pdfmetrics.stringWidth(text, self.font_mapping['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)
self.canvas.restoreState()
def drawText(self, item, divider_text="card", wrapper="no"):
card = item.card
# Skip blank cards
if card.isBlank():
return
self.canvas.saveState()
usedHeight = 0
totalHeight = item.cardHeight
# Figure out if any translation needs to be done
if wrapper == "back":
self.canvas.translate(item.cardWidth, item.cardHeight + item.tabHeight)
self.canvas.rotate(180)
if wrapper == "front":
self.canvas.translate(0, item.cardHeight + item.tabHeight + item.stackHeight)
if wrapper == "front" or wrapper == "back":
if self.options.notch_length > 0:
usedHeight += self.options.notch_height * cm
# Add 'body-top' items
drewTopIcon = False
Image_x_left = 4
if 'body-top' in self.options.cost and not card.isExpansion():
Image_x_left += self.drawCost(card, Image_x_left, totalHeight - usedHeight - 0.5 * cm)
drewTopIcon = True
Image_x_right = item.cardWidth - 4
if 'body-top' in self.options.set_icon and not card.isExpansion():
setImage = card.setImage()
if setImage:
Image_x_right -= 16
self.drawSetIcon(setImage, Image_x_right,
totalHeight - usedHeight - 0.5 * cm - 3)
drewTopIcon = True
if self.options.count:
Image_x_right -= self.drawCardCount(card, Image_x_right,
totalHeight - usedHeight - 0.5 * cm)
drewTopIcon = True
if (self.options.types and not card.isExpansion()):
# Calculate how much width have for printing
# Want centered, but number of other items can limit
left_margin = Image_x_left
right_margin = item.cardWidth - Image_x_right
worst_margin = max(left_margin, right_margin)
w = item.cardWidth / 2
textWidth = item.cardWidth - 2 * worst_margin
textWidth2 = item.cardWidth - left_margin - right_margin
# Calculate font size that will fit in the area
# Start with centering type. But if the fontSize gets too small
# use all the available space, even if it is not centered on the card
fontSize = 8
failover = False
width = stringWidth(card.types_name, self.font_mapping['Regular'], fontSize)
while width > textWidth:
fontSize -= .01
if fontSize < 6 and not failover:
# Start over using all available space left on line
textWidth = textWidth2
w = left_margin + (textWidth2 / 2)
fontSize = 8
failover = True
width = stringWidth(card.types_name, self.font_mapping['Regular'], fontSize)
# Print out the text in the right spot
h = totalHeight - usedHeight - 0.5 * cm
self.canvas.setFont(self.font_mapping['Regular'], fontSize)
if card.types_name != ' ':
self.canvas.drawCentredString(w, h, card.types_name)
drewTopIcon = True
if drewTopIcon:
usedHeight += 15
# Figure out what text is to be printed on this divider
descriptions = None
if divider_text == "card" and card.description:
# Add the card text to the divider
descriptions = card.description
elif divider_text == "rules" and card.extra:
# Add the extra rules text to the divider
descriptions = card.extra
if descriptions is None:
# No text to print, so exit early and cleanly
self.canvas.restoreState()
return
s = getSampleStyleSheet()['BodyText']
s.fontName = "Times-Roman"
if divider_text == "card" and not card.isExpansion():
s.alignment = TA_CENTER
else:
s.alignment = TA_JUSTIFY
textHorizontalMargin = .5 * cm
textVerticalMargin = .3 * cm
textBoxWidth = item.cardWidth - 2 * textHorizontalMargin
textBoxHeight = totalHeight - usedHeight - 2 * textVerticalMargin
spacerHeight = 0.2 * cm
minSpacerHeight = 0.05 * cm
if not card.isExpansion():
descriptions = self.add_inline_text(card, descriptions)
descriptions = re.split("\n", descriptions)
while True:
paragraphs = []
# this accounts for the spacers we insert between paragraphs
h = (len(descriptions) - 1) * spacerHeight
for d in descriptions:
if card.isExpansion():
dmod = d
else:
dmod = self.add_inline_images(d, s.fontSize)
try:
p = Paragraph(dmod, s)
except ValueError as e:
raise ValueError(u'Error rendering text from "{}": {} ("{}")'.format(card.name, e, dmod))
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
self.canvas.restoreState()
def drawDivider(self, item, isBack=False, horizontalMargin=-1, verticalMargin=-1):
# First save canvas state
self.canvas.saveState()
# Make sure we use the right margins
if horizontalMargin < 0:
horizontalMargin = self.options.horizontalMargin
if verticalMargin < 0:
verticalMargin = self.options.verticalMargin
# apply the transforms to get us to the corner of the current card
self.canvas.resetTransforms()
pageWidth = self.options.paperwidth - (2 * horizontalMargin)
self.canvas.translate(horizontalMargin, verticalMargin)
if isBack:
self.canvas.translate(self.options.back_offset,
self.options.back_offset_height)
pageWidth -= 2 * self.options.back_offset
item.translate(self.canvas, pageWidth, isBack)
# actual drawing
if not self.options.tabs_only:
self.drawOutline(item, isBack)
if self.options.wrapper:
wrap = "front"
isBack = False # Safety. If a wrapper, there is no backside
else:
wrap = "no"
cardText = item.textTypeFront
if isBack:
cardText = item.textTypeBack
self.drawTab(item, wrapper=wrap, backside=isBack)
if not self.options.tabs_only:
self.drawText(item, cardText, wrapper=wrap)
if item.wrapper:
self.drawTab(item, wrapper="back", backside=True)
self.drawText(item, item.textTypeBack, wrapper="back")
# retore the canvas state to the way we found it
self.canvas.restoreState()
def drawSetNames(self, pageItems):
# print sets for this page
self.canvas.saveState()
try:
# calculate the text height, font size, and orientation
maxFontsize = 12
minFontsize = 6
fontname = self.font_mapping['Regular']
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)
xPos = layout['width'] / 2
# Place at the very edge of the margin
yPos = layout['minMarginHeight']
sets = []
for item in pageItems:
setTitle = item.card.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 calculatePages(self, cards):
options = self.options
# Adjust for Vertical vs Horizontal
if options.orientation == "vertical":
options.dividerWidth, options.dividerBaseHeight = options.dominionCardHeight, options.dominionCardWidth
else:
options.dividerWidth, options.dividerBaseHeight = options.dominionCardWidth, options.dominionCardHeight
options.fixedMargins = False
options.spin = 0
options.label = options.label if 'label' in options else None
if options.label is not None:
# Set Margins
options.minmarginheight = (options.label['margin-top'] + options.label['pad-vertical']) * cm
options.minmarginwidth = (options.label['margin-left'] + options.label['pad-horizontal']) * cm
# Set Label size
options.labelHeight = (options.label['tab-height'] - 2 * options.label['pad-vertical']) * cm
options.labelWidth = (options.label['width'] - 2 * options.label['pad-horizontal']) * cm
# Set spacing between labels
options.verticalBorderSpace = (options.label['gap-vertical'] + 2 * options.label['pad-vertical']) * cm
options.horizontalBorderSpace = (options.label['gap-horizontal'] + 2 * options.label['pad-horizontal']) * cm
# Fix up other settings
options.fixedMargins = True
options.dividerBaseHeight = options.label['body-height'] * cm
options.dividerWidth = options.labelWidth
options.rotate = 0
options.dominionCardWidth = options.dividerWidth
options.dominionCardHeight = options.dividerBaseHeight
if options.orientation == "vertical":
# Spin the card. This is similar to a rotate, but given a label has a fixed location on the page
# the divider must change shape and rotation. Rotate can't be used directly,
# since that is used in the calculation of where to place the dividers on the page.
# This 'spins' the divider only, but keeps all the other calcuations the same.
options.spin = 270
# Now fix up the card dimentions.
options.dominionCardWidth = options.labelHeight + options.label['body-height'] * cm
options.dominionCardHeight = options.labelWidth - options.label['tab-height'] * cm
options.labelWidth = options.dominionCardWidth
# Need to swap now because they will be swapped again later because "vertical"
options.dominionCardWidth, options.dominionCardHeight = (options.dominionCardHeight,
options.dominionCardWidth)
# Fix up the label dimentions
if options.tab_side != "full":
options.labelWidth = options.tabwidth * cm
else:
# Margins already set
# Set Label size
options.labelHeight = .9 * cm
options.labelWidth = options.tabwidth * cm
if options.tab_side == "full" or options.labelWidth > options.dividerWidth:
options.labelWidth = options.dividerWidth
# Set spacing between labels
options.verticalBorderSpace = options.vertical_gap * cm
options.horizontalBorderSpace = options.horizontal_gap * cm
# Set Height
options.dividerHeight = options.dividerBaseHeight + options.labelHeight
# Start building up the space reserved for each divider
options.dividerWidthReserved = options.dividerWidth
options.dividerHeightReserved = options.dividerHeight
if options.wrapper:
# Adjust height for wrapper. Use the maximum thickness of any divider so we know anything will fit.
maxStackHeight = max(c.getStackHeight(options.thickness) for c in cards)
options.dividerHeightReserved = 2 * (options.dividerHeightReserved + maxStackHeight)
print("Max Card Stack Height: {:.2f}cm ".format(maxStackHeight/10.0))
# Adjust for rotation
if options.rotate == 90 or options.rotate == 270:
# for page calculations, this just means switching horizontal and vertical for these rotations.
options.dividerWidth, options.dividerHeight = options.dividerHeight, options.dividerWidth
options.dividerWidthReserved, options.dividerHeightReserved = (options.dividerHeightReserved,
options.dividerWidthReserved)
options.dividerWidthReserved += options.horizontalBorderSpace
options.dividerHeightReserved += options.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
numDividersVerticalP = int(
(options.paperheight - 2 * options.minmarginheight + options.verticalBorderSpace) /
options.dividerHeightReserved)
numDividersHorizontalP = int(
(options.paperwidth - 2 * options.minmarginwidth + options.horizontalBorderSpace) /
options.dividerWidthReserved)
numDividersVerticalL = int(
(options.paperwidth - 2 * options.minmarginwidth + options.verticalBorderSpace) /
options.dividerHeightReserved)
numDividersHorizontalL = int(
(options.paperheight - 2 * options.minmarginheight + options.horizontalBorderSpace) /
options.dividerWidthReserved)
if ((numDividersVerticalL * numDividersHorizontalL > numDividersVerticalP *
numDividersHorizontalP) and not options.fixedMargins) and options.rotate == 0:
options.numDividersVertical = numDividersVerticalL
options.numDividersHorizontal = numDividersHorizontalL
options.minHorizontalMargin = options.minmarginheight
options.minVerticalMargin = options.minmarginwidth
options.paperheight, options.paperwidth = options.paperwidth, options.paperheight
else:
options.numDividersVertical = numDividersVerticalP
options.numDividersHorizontal = numDividersHorizontalP
options.minHorizontalMargin = options.minmarginheight
options.minVerticalMargin = options.minmarginwidth
assert options.numDividersVertical > 0, "Could not vertically fit the divider on the page"
assert options.numDividersHorizontal > 0, "Could not horizontally fit the divider on the page"
if not options.fixedMargins:
# dynamically max margins
options.horizontalMargin = (options.paperwidth - options.numDividersHorizontal *
options.dividerWidthReserved + options.horizontalBorderSpace) / 2
options.verticalMargin = (options.paperheight - options.numDividersVertical *
options.dividerHeightReserved + options.verticalBorderSpace) / 2
else:
options.horizontalMargin = options.minmarginwidth
options.verticalMargin = options.minmarginheight
items = self.setupCardPlots(options, cards) # Turn cards into items to plot
self.pages = self.convert2pages(options, items) # plot items into pages
def setupCardPlots(self, options, cards=[]):
# First, set up common information for the dividers
# Doing a lot of this up front, while the cards are ordered
# just in case the dividers need to be reordered on the page.
# By setting up first, any tab or text flipping will be correct,
# even if the divider moves around a bit on the pages.
# Drawing line type
if options.cropmarks:
if 'dot' in options.linetype.lower():
lineType = 'dot' # Allow the DOTs if requested
else:
lineType = 'no_line'
else:
lineType = options.linetype.lower()
# Starting with tabs on the left, right, or centre?
if "right" in options.tab_side:
tabSideStart = CardPlot.RIGHT # right, right-alternate, right-flip
elif "left" in options.tab_side:
tabSideStart = CardPlot.LEFT # left, left-alternate, left-flip
elif "centre" in options.tab_side:
tabSideStart = CardPlot.CENTRE # centre
elif "full" == options.tab_side:
tabSideStart = CardPlot.CENTRE # full
else:
tabSideStart = CardPlot.LEFT # catch anything else
cardWidth = options.dominionCardWidth
cardHeight = options.dominionCardHeight
# Adjust for Vertical
if options.orientation == "vertical":
cardWidth, cardHeight = cardHeight, cardWidth
# Initialized CardPlot tabs
CardPlot.tabSetup(tabNumber=options.tab_number,
cardWidth=cardWidth,
cardHeight=cardHeight,
lineType=lineType,
tabWidth=options.labelWidth,
tabHeight=options.labelHeight,
start=tabSideStart,
serpentine=options.tab_serpentine,
wrapper=options.wrapper)
# Now go through all the cards and create their plotter information record...
items = []
nextTabIndex = CardPlot.tabRestart()
lastCardSet = None
reset_expansion_tabs = options.expansion_dividers and options.expansion_reset_tabs
for card in cards:
# Check if tab needs to be reset to the start
if reset_expansion_tabs and not card.isExpansion():
if lastCardSet != card.cardset_tag:
# In a new expansion, so reset the tabs to start over
nextTabIndex = CardPlot.tabRestart()
if options.tab_number > Card.sets[card.cardset_tag]['count']:
# Limit to the number of tabs to the number of dividers in the expansion
CardPlot.tabSetup(tabNumber=Card.sets[card.cardset_tag]['count'])
elif CardPlot.tabNumber != options.tab_number:
# Make sure tabs are set back to the original
CardPlot.tabSetup(tabNumber=options.tab_number)
lastCardSet = card.cardset_tag
if self.wantCentreTab(card):
# If we want centred expansion cards, then force this divider to centre
thisTabIndex = 0
else:
thisTabIndex = nextTabIndex
item = CardPlot(card,
rotation=options.spin if options.spin != 0 else options.rotate,
tabIndex=thisTabIndex,
textTypeFront=options.text_front,
textTypeBack=options.text_back,
stackHeight=card.getStackHeight(options.thickness)
)
if options.flip and (options.tab_number == 2) and (thisTabIndex != CardPlot.tabStart):
item.flipFront2Back() # Instead of flipping the tab, flip the whole divider front to back
# Before moving on, setup the tab for the next item if this tab slot was used
if thisTabIndex == nextTabIndex:
nextTabIndex = item.nextTab(nextTabIndex) # already used, so move on to the next tab
items.append(item)
return items
def convert2pages(self, options, items=[]):
# Take the layout and all the items and separate the items into pages.
# Each item will have all its plotting information filled in.
rows = options.numDividersVertical
columns = options.numDividersHorizontal
numPerPage = rows * columns
# Calculate if there is always enough room for horizontal and vertical crop marks
RoomForCropH = options.horizontalBorderSpace > 2*(options.cropmarkLength + options.cropmarkSpacing) * cm
RoomForCropV = options.verticalBorderSpace > 2*(options.cropmarkLength + options.cropmarkSpacing) * cm
items = split(items, numPerPage)
pages = []
for pageNum, pageItems in enumerate(items):
page = []
for i in range(numPerPage):
if pageItems and i < len(pageItems):
# Given a CardPlot object called item, its number on the page, and the page number
# Return/set the items x,y,rotation, crop mark settings, and page number
# For x,y assume the canvas has already been adjusted for the margins
x = i % columns
y = (rows - 1) - (i // columns)
pageItems[i].x = x * options.dividerWidthReserved
pageItems[i].y = y * options.dividerHeightReserved
pageItems[i].cropOnTop = (y == rows - 1) or RoomForCropV
pageItems[i].cropOnBottom = (y == 0) or RoomForCropV
pageItems[i].cropOnLeft = (x == 0) or RoomForCropH
pageItems[i].cropOnRight = (x == columns - 1) or RoomForCropH
# pageItems[i].rotation = 0
pageItems[i].page = pageNum + 1
page.append(pageItems[i])
pages.append((options.horizontalMargin, options.verticalMargin, page))
return pages
def drawDividers(self, cards=[]):
if not self.pages:
self.calculatePages(cards)
# Now go page by page and print the dividers
for pageNum, pageInfo in enumerate(self.pages):
hMargin, vMargin, page = pageInfo
# Front page footer
if not self.options.no_page_footer and (
not self.options.tabs_only and
self.options.order != "global"):
self.drawSetNames(page)
# Front page
for item in page:
# print the dividor
self.drawDivider(item, isBack=False, horizontalMargin=hMargin, verticalMargin=vMargin)
self.canvas.showPage()
if pageNum + 1 == self.options.num_pages:
break
if self.options.tabs_only or self.options.text_back == "none" or self.options.wrapper:
# Don't print the sheets with the back of the dividers
continue
# back page footer
if not self.options.no_page_footer and self.options.order != "global":
self.drawSetNames(page)
# Back page
for item in page:
# print the dividor
self.drawDivider(item, isBack=True, horizontalMargin=hMargin, verticalMargin=vMargin)
self.canvas.showPage()
if pageNum + 1 == self.options.num_pages:
break