diff --git a/extensions/fablabchemnitz/playing_cards/playing_cards.inx b/extensions/fablabchemnitz/playing_cards/playing_cards.inx new file mode 100755 index 00000000..4e8a729c --- /dev/null +++ b/extensions/fablabchemnitz/playing_cards/playing_cards.inx @@ -0,0 +1,131 @@ + + + Playing Cards + fablabchemnitz.de.playing_cards + + + + + + 2.5 + + + + + + + + + + 3.5 + + + + + + + + + + 1 + + + + + + + + + + + + 0 + + + + + + + + + + 5 + + + + + + + + + + 1 + + + + + + + + + + 5 + + + + + + + + + + 0 + + + + + + + + + + + + + + + + true + + + + 5 + + + + + + + + + true + true + true + true + true + false + true + + + + all + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/playing_cards/playing_cards.py b/extensions/fablabchemnitz/playing_cards/playing_cards.py new file mode 100755 index 00000000..2642442e --- /dev/null +++ b/extensions/fablabchemnitz/playing_cards/playing_cards.py @@ -0,0 +1,862 @@ +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[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)' + r'(?P[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()