This repository has been archived on 2023-03-25. You can view files and clone it, but cannot push or open issues or pull requests.
mightyscape-1.1-deprecated/extensions/fablabchemnitz/playing_cards/playing_cards.py

863 lines
31 KiB
Python
Raw Normal View History

2021-07-23 02:36:56 +02:00
from math import ceil, floor
from lxml import etree
import re
import inkex
# Constants
# =========
# A unit is represented as a conversion factor relative to the pixel unit. The
# keys must be identical to the optiongroup options defined in the .inx file.
UNITS = {
"px": 1.0,
"pt": 96.0 / 72.0,
"in": 96.0,
"cm": 96.0 / 2.54,
"mm": 96.0 / 25.4
}
# EPSILON is used as a threshold by the rounding functions
EPSILON = 1e-3
# FOLD_LINE_TYPES defines the accepted values for horizontal and vertical
# fold lines that can be set on the command line.
NO_FOLD_LINE = "NoFoldLine"
HORIZONTAL_FOLD_LINE = "HorizontalFoldLine"
VERTICAL_FOLD_LINE = "VerticalFoldLine"
FOLD_LINE_TYPES = [NO_FOLD_LINE, HORIZONTAL_FOLD_LINE, VERTICAL_FOLD_LINE]
# Functions that change positions in some way
# ===========================================
def round_up(value, grid_size):
"""
Return the smallest grid point that is greater or equal to the value.
:type value: float
:type grid_size: float
:rtype: float
"""
try:
return ceil(value / grid_size - EPSILON) * grid_size
except ZeroDivisionError:
return value
def round_down(value, grid_size):
"""
Return the greatest grid point that is less or equal to the value.
:type value: float
:type grid_size: float
:rtype: float
"""
try:
return floor(value / grid_size + EPSILON) * grid_size
except ZeroDivisionError:
return value
def mirror_at(value, at):
"""
Reflect the value at a given point.
:type value: float
:type at: float
:rtype: float
"""
return 2.0 * at - value
# Functions related to quantities and units
# =========================================
def convert_unit(source_unit, target_unit):
"""
Returns a factor that converts from one unit to another.
:type source_unit: str | float
:type target_unit: str | float
:rtype: float
"""
# If the units are the same the conversion factor is obviously 1
if source_unit == target_unit:
return 1.0
# If the unit is given as a float nothing needs to be done. Otherwise we
# try to find the unit and its float value in the dictionary of valid
# units.
if not isinstance(source_unit, float):
if source_unit not in UNITS.keys():
raise ValueError("unexpected unit \"" + source_unit + "\"")
source_unit = UNITS[source_unit]
if not isinstance(target_unit, float):
if target_unit not in UNITS.keys():
raise ValueError("unexpected unit \"" + target_unit + "\"")
target_unit = UNITS[target_unit]
return source_unit / target_unit
def make_quantity(magnitude, unit):
"""
Create a quantity from a magnitude and a unit.
:type magnitude: float
:type unit: str
"""
return "{0}{1}".format(magnitude, unit)
def split_quantity(quantity):
"""
Split a quantity into its magnitude and unit and return them as a tuple.
:type quantity: str
:rtype: (float, str) | (float, NoneType)
"""
# Matches a floating point number optionally followed by letters. The
# floating point number is the magnitude and the letters are the unit.
pattern = re.compile(r'(?P<magnitude>[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)'
r'(?P<unit>[a-zA-Z]+)?')
match = re.match(pattern, quantity)
if match:
return (float(match.group("magnitude")), match.group("unit"))
else:
return (0, None)
def convert_quantity(quantity, target_unit):
"""
Convert the unit of a quantity to another unit.
:type quantity: str
:type taget_unit: str | float
:rtype: str
"""
return "{0}{1}".format(convert_magnitude(quantity, target_unit), target_unit)
def convert_magnitude(quantity, target_unit):
"""
Convert the unit of a quantity to another unit and return only the new magnitude.
:type quantity: str
:type target_unit: str | float
:rtype: float
"""
magnitude, source_unit = split_quantity(quantity)
new_magnitude = magnitude * convert_unit(source_unit, target_unit)
return new_magnitude
# Functions related to the placement of cards
# ===========================================
def calculate_positions_without_fold_line(page_size, margin_size, card_size,
bleed_size, grid_size, min_spacing,
grid_aligned):
"""
Position cards along one direction of the page without a fold line.
The calculated positions are the positions of the right edges or bottom
edges of the cards. The other edges and the positions of the bleeds can be
easily derived by adding card_size, -bleed_size, and card_size+bleed_size.
All sizes and spacings must be given as magnitudes, i.e. without units.
Their units are assumed to be identical but can be arbitrary.
:param page_size: The width or height of the page.
:type page_size: float
:param margin_size: The empty margin of the page. Nothing will be placed in
the margin except for the frame.
:type margin_size: float
:param card_size: The width or height of each card.
:type card_size: float
:param bleed_size: The bleed around each card. This can be zero.
:type bleed_size: float
:param grid_size: The size of the alignment grid. The value is ignored if
grid_aligned is False.
:type grid_size: float
:param min_spacing: The minimum distance between two cards.
:type min_spacing: float
:param grid_aligned: Whether or not the beginning of a card should be on a
grid point.
:type grid_aligned: bool
:return: A list containing the beginnings of each card
:rtype: [float]
"""
# The bleed of the first card begins where the page margin ends. The card
# is then moved to the next grid point if grid_aligned is True.
card_begin = margin_size + bleed_size
if grid_aligned:
card_begin = round_up(card_begin, grid_size)
card_end = card_begin + card_size
# There are to bleeds between the end of the first card and the beginning
# of the next. The spacing between two cards is two bleeds or min_spacing,
# whichever is greater. If grid_aligned is True the next card is moved even
# farther away so that it begins at the next grid point.
spacing = max(min_spacing, 2.0 * bleed_size)
if grid_aligned:
spacing = round_up(card_end + spacing, grid_size) - card_end
# We add cards and spacings until we run out of enough empty space.
cards = []
remaining = 0
while True:
card_end = card_begin + card_size
next_remaining = page_size - margin_size - card_end - bleed_size
if next_remaining < 0:
break
remaining = next_remaining
cards.append(card_begin)
card_begin = card_end + spacing
# Shift everything towards the center of the page.
shift = remaining / 2.0
if grid_aligned:
shift = round_down(shift, grid_size)
cards = [card + shift for card in cards]
return cards
def calculate_positions_with_fold_line(page_size, margin_size, card_size,
bleed_size, grid_size, min_spacing,
min_fold_line_spacing, grid_aligned):
"""
Position the cards along one direction of the page with a central fold line.
The calculated positions are the positions of the right edges or bottom
edges of the cards. The other edges and the positions of the bleeds can be
easily derived by adding card_size, -bleed_size, and card_size+bleed_size.
All sizes and spacings must be given as magnitudes, i.e. without units.
Their units are assumed to be identical but can be arbitrary.
:param page_size: The width or height of the page.
:type page_size: float
:param margin_size: The empty margin of the page. Nothing will be placed in
the margin.
:type margin_size: float
:param card_size: The width or height of each card.
:type card_size: float
:param bleed_size: The bleed around each card. This can be zero.
:type bleed_size: float
:param grid_size: The size of the alignment grid. The value is ignored if
grid_aligned is False.
:type grid_size: float
:param min_spacing: The minimum distance between two cards.
:type min_spacing: float
:param min_fold_line_spacing: The minimum distance between a card and the
fold line.
:type min_fold_line_spacing: float
:param grid_aligned: Whether or not the beginning of a card should be on a
grid point.
:type grid_aligned: bool
:return: A tuple with a list containing the beginnings of each card and the
position of the fold line.
:rtype: ([float], float)
"""
# The spacing between the two central cards at the fold line must be at
# at least 2*bleed_size or 2*min_fold_line_spacing or min_spacing,
# whichever is the greatest.
central_spacing = max(2.0 * min_fold_line_spacing,
max(min_spacing, 2.0 * bleed_size))
# First we assume that the fold line is at the center of the page. This
# might change a bit later if we want grid alignment. We then place the
# first card before the fold line so that there is an empty space of
# central_spacing/2 between the card and the fold line.
card_begin = (page_size - central_spacing) / 2.0 - card_size
if grid_aligned:
card_begin = round_down(card_begin, grid_size)
card_end = card_begin + card_size
# The card on the other side can be placed by mirroring the first card at
# the fold line. But this card is not neccessarily grid aligned. We fix that
# by increasing the central spacing so that the first card on the other side
# of the fold line is also grid aligned.
if grid_aligned:
central_spacing = round_up(
card_end + central_spacing, grid_size) - card_end
# The fold line should not be at the center of the page but in the middle
# between the two central cards. If we don't use grid alignment then this
# is also the center of the page.
fold_line = card_end + central_spacing / 2.0
# The spacing between all remaining cards might be different because we
# don't use min_fold_line_spacing. But the calculation remains the same as
# for the two central cards.
spacing = max(min_spacing, 2.0 * bleed_size)
if grid_aligned:
spacing = round_up(card_end + spacing, grid_size) - card_end
# Now that we have calculated all spacings we start adding cards to both
# sides of the fold line beginning at the center and moving outwards.
cards = []
while True:
if card_begin < margin_size:
break
cards.append(card_begin)
cards.append(mirror_at(card_end, fold_line))
card_begin -= card_size + spacing
card_end = card_begin + card_size
# We sort the positions of the cards so that the positions start with the
# lowest and end with the highest value.
cards.sort()
return (cards, fold_line)
class PlayingCardsExtension(inkex.EffectExtension):
"""
Implements the interface for Inkscape addons.
An instance of this class is created in main(). __init__() sets up the
OptionParser provided by the base class to recognize all needed command
line parameters. Then in main() inkex.Effect.run() is called which then
parses the command line and calls effect(). This is where we do our work.
"""
# Constants passed from the command line
PAGE_WIDTH = None
PAGE_HEIGHT = None
CARD_WIDTH = None
CARD_HEIGHT = None
BLEED_SIZE = None
MIN_CARD_SPACING = None
CROP_MARK_SPACING = None
MIN_FOLD_LINE_SPACING = None
PAGE_MARGIN = None
GRID_SIZE = None
ALIGN_TO_GRID = None
FOLD_LINE_TYPE = None
FRAME_SPACING = None
DRAW_GUIDES = None
DRAW_CARDS = None
DRAW_BLEEDS = None
DRAW_CROP_LINES = None
DRAW_FOLD_LINE = None
DRAW_PAGE_MARGIN = None
DRAW_FRAME = None
USER_UNIT = None # The unit used in the document
horizontal_card_positions = None # Calculated horizontal positions
vertical_card_positions = None # Calculated vertical positions
fold_line_position = None # Calculated position of the fold line
def add_arguments(self, pars):
"""
Initialize the OptionParser with recognized parameters.
The option names must be identical to those defined in the .inx file.
The option values are later used to initialize the class constants.
"""
pars.add_argument("--pageName", type=str)
pars.add_argument("--cardWidth", type=float)
pars.add_argument("--cardWidthUnit", choices=UNITS.keys())
pars.add_argument("--cardHeight", type=float)
pars.add_argument("--cardHeightUnit", choices=UNITS.keys())
pars.add_argument("--bleedSize", type=float, action="store")
pars.add_argument("--bleedSizeUnit", choices=UNITS.keys())
pars.add_argument("--minCardSpacing", type=float)
pars.add_argument("--minCardSpacingUnit", choices=UNITS.keys())
pars.add_argument("--cropMarkSpacing", type=float)
pars.add_argument("--cropMarkSpacingUnit", choices=UNITS.keys())
pars.add_argument("--minFoldLineSpacing", type=float)
pars.add_argument("--minFoldLineSpacingUnit", choices=UNITS.keys())
pars.add_argument("--pageMargin", type=float)
pars.add_argument("--pageMarginUnit", choices=UNITS.keys())
pars.add_argument("--frameSpacing", type=float)
pars.add_argument("--frameSpacingUnit", choices=UNITS.keys())
pars.add_argument("--gridSize", type=float)
pars.add_argument("--gridSizeUnit", choices=UNITS.keys())
pars.add_argument("--gridAligned", type=inkex.Boolean)
pars.add_argument("--foldLineType", choices=FOLD_LINE_TYPES)
pars.add_argument("--drawGuides", type=inkex.Boolean)
pars.add_argument("--drawCards", type=inkex.Boolean)
pars.add_argument("--drawBleeds", type=inkex.Boolean)
pars.add_argument("--drawCropLines", type=inkex.Boolean)
pars.add_argument("--drawFoldLine", type=inkex.Boolean)
pars.add_argument("--drawPageMargin", type=inkex.Boolean)
pars.add_argument("--drawFrame", type=inkex.Boolean)
def init_user_unit(self):
"""
Determine the user unit from the document contents.
"""
root = self.document.getroot()
view_box = root.get("viewBox")
# If the document has a valid viewBox we try to derive the user unit
# from that.
valid_view_box = view_box and len(view_box.split()) == 4
if valid_view_box:
view_box = root.get("viewBox").split()
view_box_width, view_box_width_unit = split_quantity(view_box[2])
# If the viewBox has a unit use that.
if view_box_width_unit:
self.USER_UNIT = view_box_width_unit
# If the viewBox has no unit derive the unit from the ratio between
# the document width and the viewBox width.
else:
document_width, document_width_unit = split_quantity(
self.document_width())
self.USER_UNIT = document_width / view_box_width
if document_width_unit:
self.USER_UNIT *= UNITS[document_width_unit]
# If the document has no valid viewBox we try to derive the user unit
# from the document width.
else:
document_width, document_width_unit = split_quantity(
self.document_width())
if document_width_unit:
self.USER_UNIT = UNITS[document_width_unit]
else:
# This might be problematic because v0.91 uses 90dpi and v0.92
# uses 96dpi
self.USER_UNIT = UNITS["px"]
def init_constants(self):
"""
Initialize the class constants from the OptionParser values and the
document contents.
This converts all quantities from the unit given on the command line to
the user unit.
"""
self.PAGE_WIDTH = self.to_user_unit(self.document_width())
self.PAGE_HEIGHT = self.to_user_unit(self.document_height())
self.CARD_WIDTH = self.to_user_unit(
make_quantity(self.options.cardWidth,
self.options.cardWidthUnit))
self.CARD_HEIGHT = self.to_user_unit(
make_quantity(self.options.cardHeight,
self.options.cardHeightUnit))
self.BLEED_SIZE = self.to_user_unit(
make_quantity(self.options.bleedSize,
self.options.bleedSizeUnit))
self.GRID_SIZE = self.to_user_unit(
make_quantity(self.options.gridSize,
self.options.gridSizeUnit))
self.MIN_CARD_SPACING = self.to_user_unit(
make_quantity(self.options.minCardSpacing,
self.options.minCardSpacingUnit))
self.CROP_MARK_SPACING = self.to_user_unit(
make_quantity(self.options.cropMarkSpacing,
self.options.cropMarkSpacingUnit))
self.MIN_FOLD_LINE_SPACING = self.to_user_unit(
make_quantity(self.options.minFoldLineSpacing,
self.options.minFoldLineSpacingUnit))
self.PAGE_MARGIN = self.to_user_unit(
make_quantity(self.options.pageMargin,
self.options.pageMarginUnit))
self.FRAME_SPACING = self.to_user_unit(
make_quantity(self.options.frameSpacing,
self.options.frameSpacingUnit))
self.ALIGN_TO_GRID = self.options.gridAligned
self.FOLD_LINE_TYPE = self.options.foldLineType
self.DRAW_GUIDES = self.options.drawGuides
self.DRAW_CARDS = self.options.drawCards
self.DRAW_BLEEDS = self.options.drawBleeds
self.DRAW_CROP_LINES = self.options.drawCropLines
self.DRAW_FOLD_LINE = self.options.drawFoldLine
self.DRAW_PAGE_MARGIN = self.options.drawPageMargin
self.DRAW_FRAME = self.options.drawFrame
def effect(self):
self.init_user_unit()
self.init_constants()
self.calculate_positions()
# Create one layer for the things that we want to print and another
# layer for things that we don't want to print but are useful while
# working on the cards.
non_printing_layer = self.create_layer("(template) non printing")
printing_layer = self.create_layer("(template) printing")
if self.DRAW_GUIDES:
self.create_guides()
if self.DRAW_CARDS:
self.create_cards(non_printing_layer)
if self.DRAW_BLEEDS:
self.create_bleeds(non_printing_layer)
if self.DRAW_CROP_LINES:
self.create_crop_lines(printing_layer)
if self.DRAW_FOLD_LINE:
self.create_fold_line(printing_layer)
if self.DRAW_PAGE_MARGIN:
self.create_margin(non_printing_layer)
if self.DRAW_FRAME:
self.create_frame(printing_layer)
def to_user_unit(self, quantity):
"""
Convert a quantity to the user unit and return its magnitude.
:type quantity: str
:rtype: float
"""
return convert_magnitude(quantity, self.USER_UNIT)
def document_width(self):
"""
Return the document width.
The width is read from the document. It may or may not contain a unit.
"""
return self.document.getroot().get("width")
def document_height(self):
"""
Return the document height.
The height is read from the document. It may or may not contain a unit.
"""
return self.document.getroot().get("height")
def calculate_positions(self):
"""
Calculate the horizontal and vertical positions of all cards.
The results are stored in self.horizontal_card_positions,
self.vertical_card_positions, and self.fold_line_position.
"""
if self.FOLD_LINE_TYPE == VERTICAL_FOLD_LINE:
self.horizontal_card_positions, self.fold_line_position = \
calculate_positions_with_fold_line(
self.PAGE_WIDTH,
self.PAGE_MARGIN,
self.CARD_WIDTH,
self.BLEED_SIZE,
self.GRID_SIZE,
self.MIN_CARD_SPACING,
self.MIN_FOLD_LINE_SPACING,
self.ALIGN_TO_GRID)
else:
self.horizontal_card_positions = \
calculate_positions_without_fold_line(
self.PAGE_WIDTH,
self.PAGE_MARGIN,
self.CARD_WIDTH,
self.BLEED_SIZE,
self.GRID_SIZE,
self.MIN_CARD_SPACING,
self.ALIGN_TO_GRID)
if self.FOLD_LINE_TYPE == HORIZONTAL_FOLD_LINE:
self.vertical_card_positions, self.fold_line_position = \
calculate_positions_with_fold_line(
self.PAGE_HEIGHT,
self.PAGE_MARGIN,
self.CARD_HEIGHT,
self.BLEED_SIZE,
self.GRID_SIZE,
self.MIN_CARD_SPACING,
self.MIN_FOLD_LINE_SPACING,
self.ALIGN_TO_GRID)
else:
self.vertical_card_positions = \
calculate_positions_without_fold_line(
self.PAGE_HEIGHT,
self.PAGE_MARGIN,
self.CARD_HEIGHT,
self.BLEED_SIZE,
self.GRID_SIZE,
self.MIN_CARD_SPACING,
self.ALIGN_TO_GRID)
# Functions related to the structure of the document
# ==================================================
def create_group(self, parent, label):
"""
Create a new group in the svg document.
:type parent: lxml.etree._Element
:type label: str
:rtype: lxml.etree._Element
"""
group = etree.SubElement(parent, "g")
group.set(inkex.addNS("label", "inkscape"), label)
return group
def create_layer(self, label, is_visible=True, is_locked=True):
"""
Create a new layer in the svg document.
:type label: str
:rtype: lxml.etree._Element
"""
layer = self.create_group(self.document.getroot(), label)
layer.set(inkex.addNS("groupmode", "inkscape"), "layer")
# The Inkscape y-axis runs from bottom to top, the SVG y-axis runs from
# top to bottom. Therefore we need to transform all y coordinates.
layer.set(
"transform", "matrix(1 0 0 -1 0 {0})".format(self.PAGE_HEIGHT))
# Don't show the layer contents
if not is_visible:
layer.set("style", "display:none")
# Lock the layer
if is_locked:
layer.set(inkex.addNS("insensitive", "sodipodi"), "true")
return layer
# Functions related to the contents of the document
# =================================================
def create_guide(self, x, y, orientation):
"""
Create an arbitrary guide.
:type x: float
:type y: float
:type orientation: str
"""
view = self.document.getroot().find(inkex.addNS("namedview", "sodipodi"))
guide = etree.SubElement(view, inkex.addNS("guide", "sodipodi"))
guide.set("orientation", orientation)
guide.set("position", "{0},{1}".format(x, y))
def create_horizontal_guide(self, y):
"""
Create a horizontal guide.
"""
self.create_guide(0, y, "0,1")
def create_vertical_guide(self, x):
"""
Create a vertical guide.
"""
self.create_guide(x, 0, "1,0")
def create_guides(self):
"""
Create guides at all sides of all cards and bleeds.
"""
for x in self.horizontal_card_positions:
self.create_vertical_guide(x)
self.create_vertical_guide(x + self.CARD_WIDTH)
if self.BLEED_SIZE > 0:
self.create_vertical_guide(x - self.BLEED_SIZE)
self.create_vertical_guide(x + self.CARD_WIDTH + self.BLEED_SIZE)
for y in self.vertical_card_positions:
self.create_horizontal_guide(y)
self.create_horizontal_guide(y + self.CARD_HEIGHT)
if self.BLEED_SIZE > 0:
self.create_horizontal_guide(y - self.BLEED_SIZE)
self.create_horizontal_guide(y + self.CARD_HEIGHT + self.BLEED_SIZE)
def create_bleeds(self, parent):
"""
Creates a rectangle for each bleed.
:type parent: lxml.etree._Element
"""
if self.BLEED_SIZE <= 0:
return
attributes = {"x": str(-self.BLEED_SIZE),
"y": str(-self.BLEED_SIZE),
"width": str(self.CARD_WIDTH + 2.0 * self.BLEED_SIZE),
"height": str(self.CARD_HEIGHT + 2.0 * self.BLEED_SIZE),
"stroke": "black",
"stroke-width": str(self.to_user_unit("0.25pt")),
"fill": "none"}
for y in self.vertical_card_positions:
for x in self.horizontal_card_positions:
attributes["transform"] = "translate({0},{1})".format(x, y)
etree.SubElement(parent, "rect", attributes)
def create_cards(self, parent):
"""
Create a rectangle for each card.
:type parent: lxml.etree._Element
"""
attributes = {"x": str(0),
"y": str(0),
"width": str(self.CARD_WIDTH),
"height": str(self.CARD_HEIGHT),
"stroke": "black",
"stroke-width": str(self.to_user_unit("0.25pt")),
"fill": "none"}
for y in self.vertical_card_positions:
for x in self.horizontal_card_positions:
attributes["transform"] = "translate({0},{1})".format(x, y)
etree.SubElement(parent, "rect", attributes)
def create_fold_line(self, parent):
"""
Create a horizontal or vertical fold line.
:type parent: lxml.etree._Element
"""
attributes = {"stroke": "black",
"stroke-width": str(self.to_user_unit("0.25pt")),
"fill": "none"}
if self.FOLD_LINE_TYPE == HORIZONTAL_FOLD_LINE:
attributes["d"] = "M 0,{0} H {1}".format(
self.fold_line_position, self.PAGE_WIDTH)
elif self.FOLD_LINE_TYPE == VERTICAL_FOLD_LINE:
attributes["d"] = "M {0},0 V {1}".format(
self.fold_line_position, self.PAGE_HEIGHT)
else:
return
etree.SubElement(parent, "path", attributes)
def create_crop_lines(self, parent):
"""
Create horizontal and vertical crop lines.
:type parent: lxml.etree._Element
"""
attributes = {"stroke": "black",
"stroke-width": str(self.to_user_unit("0.25pt")),
"fill": "none"}
# (begin, end) pairs for vertical crop line between bleeds
pairs = []
begin = 0
for y in self.vertical_card_positions:
end = y - self.BLEED_SIZE - self.CROP_MARK_SPACING
# Only add lines if they fit between two bleeds
if end - begin >= EPSILON:
pairs.append((begin, end))
begin = end + self.CARD_HEIGHT \
+ 2.0 * self.BLEED_SIZE \
+ 2.0 * self.CROP_MARK_SPACING
pairs.append((begin, self.PAGE_HEIGHT))
# One crop line consists of many short strokes
attributes["d"] = " ".join(["M 0,{0} 0,{1}".format(begin, end)
for (begin, end) in pairs])
# Shifted copies of the crop line
for x in self.horizontal_card_positions:
attributes["transform"] = "translate({0},0)".format(x)
etree.SubElement(parent, "path", attributes)
attributes["transform"] = "translate({0},0)".format(
x + self.CARD_WIDTH)
etree.SubElement(parent, "path", attributes)
# (begin, end) pairs for horizontal crop line between bleeds
pairs = []
begin = 0
for x in self.horizontal_card_positions:
end = x - self.BLEED_SIZE - self.CROP_MARK_SPACING
# Only add lines if they fit between two bleeds
if end - begin >= EPSILON:
pairs.append((begin, end))
begin = end + self.CARD_WIDTH \
+ 2.0 * self.BLEED_SIZE \
+ 2.0 * self.CROP_MARK_SPACING
pairs.append((begin, self.PAGE_WIDTH))
# One crop line consists of many short strokes
attributes["d"] = " ".join(["M {0},0 {1},0".format(begin, end)
for (begin, end) in pairs])
# Shifted copies of the crop line
for y in self.vertical_card_positions:
attributes["transform"] = "translate(0,{0})".format(y)
etree.SubElement(parent, "path", attributes)
attributes["transform"] = "translate(0,{0})".format(
y + self.CARD_HEIGHT)
etree.SubElement(parent, "path", attributes)
def create_margin(self, parent):
"""
Create a rectangle for the page margin.
:type parent: lxml.etree._Element
"""
if self.PAGE_MARGIN <= 0:
return
attributes = {"x": str(self.PAGE_MARGIN),
"y": str(self.PAGE_MARGIN),
"width": str(self.PAGE_WIDTH - 2.0 * self.PAGE_MARGIN),
"height": str(self.PAGE_HEIGHT - 2.0 * self.PAGE_MARGIN),
"stroke": "black",
"stroke-width": str(self.to_user_unit("0.25pt")),
"stroke-dasharray": "0.5,0.5",
"fill": "none"}
etree.SubElement(parent, "rect", attributes)
def create_frame(self, parent):
"""
Create a frame around the cards.
"""
# If we don't have any cards we can't draw a frame around them
if len(self.horizontal_card_positions) == 0 or \
len(self.vertical_card_positions) == 0:
return
XMIN = min(self.horizontal_card_positions)
XMAX = max(self.horizontal_card_positions)
YMIN = min(self.vertical_card_positions)
YMAX = max(self.vertical_card_positions)
LEFT = XMIN - self.FRAME_SPACING
BOTTOM = YMIN - self.FRAME_SPACING
WIDTH = XMAX - XMIN + self.CARD_WIDTH + 2 * self.FRAME_SPACING
HEIGHT = YMAX - YMIN + self.CARD_HEIGHT + 2 * self.FRAME_SPACING
attributes = {"x": str(LEFT),
"y": str(BOTTOM),
"width": str(WIDTH),
"height": str(HEIGHT),
"stroke": "black",
"stroke-width": str(self.to_user_unit("0.25pt")),
"fill": "none"}
etree.SubElement(parent, "rect", attributes)
def main():
PlayingCardsExtension().run()
if __name__ == '__main__':
main()