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()