Paul Molodowitch c7558d59e9 Fix setNames intruding into dividers when back_offset_height given negative value (#262)
* consolidate code for drawing backsides
* drawSetNames: no need to calculate xPos / yPos per card
* Account for back_offset when calculating set name placement

Without this, if you had a negative back_offset_height (or
positive back_offset), it was possible to print set names that
would intrude onto the divider area on the backs

* draw setNames along side (left or bottom) with more space

This both provides more cushion, and makes it less likely that
location will change between front + back (due to back_offset_height)
2019-07-01 16:40:58 +02:00

2014 lines
80 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 -= 0.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
and self.options.tab_name_align == "edge"
)
):
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 -= 0.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 = 0.5 * cm
textVerticalMargin = 0.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, backside=False):
# 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
layouts = [
{
"rotation": 0,
"minMarginHeight": self.options.minVerticalMargin,
"totalMarginHeight": self.options.verticalMargin
+ (self.options.back_offset_height if backside else 0),
"width": self.options.paperwidth,
},
{
"rotation": 90,
"minMarginHeight": self.options.minHorizontalMargin,
"totalMarginHeight": self.options.horizontalMargin
+ (-self.options.back_offset if backside else 0),
"width": self.options.paperheight,
},
]
# Pick whether to print setnames horizontally along bottom
# (i=0) or vertically along left (i=1). We pick whichever has more
# space.
fontsize = 0
maxAvailableMargin = 0
layoutIndex = -1
for i, layout in enumerate(layouts):
availableMargin = (
layout["totalMarginHeight"] - layout["minMarginHeight"]
)
if availableMargin > maxAvailableMargin:
maxAvailableMargin = availableMargin
fontsize = availableMargin / fontHeightRelative
fontsize = min(maxFontsize, fontsize)
layoutIndex = i
if fontsize < minFontsize:
import warnings
warnings.warn("Not enough space to display set names")
return
layout = layouts[layoutIndex]
self.canvas.setFont(fontname, fontsize)
# 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
sets = []
for item in pageItems:
setTitle = item.card.cardset.title()
if setTitle not in sets:
sets.append(setTitle)
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 = 0.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
drawFooter = not self.options.no_page_footer and (
not self.options.tabs_only and self.options.order != "global"
)
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
backSides = [False]
else:
backSides = [False, True]
for isBack in backSides:
# Page footer
if drawFooter:
self.drawSetNames(page, isBack)
# Page
for item in page:
# print the dividor
self.drawDivider(
item,
isBack=isBack,
horizontalMargin=hMargin,
verticalMargin=vMargin,
)
self.canvas.showPage()
if pageNum + 1 == self.options.num_pages:
break