From 295c274b3048aa3dc0b72ffcdde5626fac063922 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Wed, 17 Sep 2025 13:58:12 +0200 Subject: [PATCH] Add support for drawing cutmarks on badges, not just borders Cutmarks are what most printers want. Instead of having to hardcode them into individual badges, add system support for drawing them automatically. In relation to this, also add support for calculating bleed for both cutmarks and borders --- docs/confreg/skinning.md | 24 ++++++++++++--- postgresqleu/confreg/jinjapdf.py | 53 +++++++++++++++++++++++++++----- postgresqleu/confreg/reports.py | 11 +++++-- template/confreg/reports.html | 14 +++++++-- 4 files changed, 85 insertions(+), 17 deletions(-) diff --git a/docs/confreg/skinning.md b/docs/confreg/skinning.md index e42096e7..c12011a3 100644 --- a/docs/confreg/skinning.md +++ b/docs/confreg/skinning.md @@ -293,11 +293,25 @@ system) in format `json`. At the root of the json structure, two elements have to be defined: width and height. All positions and sizes are defined in mm. -If the element `border` is set to *true*, a thin black border will be -drawn around the outer edges (for cutting). If the element -is set to *false*, no border will be printed. If the element is not set -at all, printing will be controlled from the form field when building the -badges, and be off when building tickets. +The element `border` controls borders and cutmarks for the badge. +If it is set to *border* or to *true*, a thin black border will be +drawn around the outer edges (for manual cutting). + +If `border` is set to *cutmarks* then outside cutmarks will be drawn +on the badge. These marks will by defaut be *10mm* long, and placed *3mm* +from the edge of the badge (with the intersection being right at the +corner of the badge, of course). The length and offset can be overridden +by setting `cutmark_length` and `cutmark_offset` respectively. + +The value of the `border` element can be overridden from the form field +when building the badges from the web. + +if the element `bleed` is set to a value, border and cutmarks will be +adjusted to bleed this much on each side of the badge. In practive, +this means adjusting the border/marks inward by this many mm. For the +resulting badge to be the expected size, the total size of the badge +(as specified in `width` and `height`) should be increased by *2 * +bleed* and the badge elements adjusted for that. If the element `forcebreaks` is set to *true*, a pagebreak will be forced between each badge, making sure there is only one badge per diff --git a/postgresqleu/confreg/jinjapdf.py b/postgresqleu/confreg/jinjapdf.py index f10be6e1..2b51ead1 100755 --- a/postgresqleu/confreg/jinjapdf.py +++ b/postgresqleu/confreg/jinjapdf.py @@ -27,6 +27,10 @@ try: except ImportError: import contextutil + +DEFAULT_CUTMARK_LENGTH = 8 +DEFAULT_CUTMARK_OFS = 3 + alignments = { 'left': TA_LEFT, 'center': TA_CENTER, @@ -58,9 +62,6 @@ class JinjaFlowable(Flowable): self.hAlign = 'CENTER' def draw(self): - if self.js.get('border', False): - self.canv.rect(0, 0, self.width, self.height) - for e in self.js['elements']: if e == {}: continue @@ -69,6 +70,29 @@ class JinjaFlowable(Flowable): f(e) else: raise Exception("Unknown type %s" % e['type']) + self._draw_border() + + def _draw_border(self): + if self.js.get('border', False) in ('border', True, 1): + self.canv.rect(-1, -1, self.width + 2, self.height + 2) + elif self.js.get('border', False) == 'cutmarks': + cmlength = self.js.get('cutmark_length', DEFAULT_CUTMARK_LENGTH) * mm + cmofs = self.js.get('cutmark_offset', DEFAULT_CUTMARK_OFS) * mm + cmpos = cmlength + cmofs + bleed = self.js.get('bleed', 0) * mm + + # Bottom left + self.canv.line(-cmpos + bleed, bleed, -cmofs + bleed, bleed) + self.canv.line(bleed, -cmpos + bleed, bleed, -cmofs + bleed) + # Bottom right + self.canv.line(self.width + cmofs - bleed, bleed, self.width + cmpos - bleed, bleed) + self.canv.line(self.width - bleed, -cmpos + bleed, self.width - bleed, -cmofs + bleed) + # Top left + self.canv.line(-cmpos + bleed, self.height - bleed, -cmofs + bleed, self.height - bleed) + self.canv.line(bleed, self.height + cmofs - bleed, bleed, self.height + cmpos - bleed) + # Top right + self.canv.line(self.width + cmofs - bleed, self.height - bleed, self.width + cmpos - bleed, self.height - bleed) + self.canv.line(self.width - bleed, self.height + cmofs - bleed, self.width - bleed, self.height + cmpos - bleed) def calc_y(self, o): return self.height - getmm(o, 'y') - getmm(o, 'height') @@ -325,8 +349,14 @@ class JinjaRenderer(object): else: raise Exception("JSON parse failed.") - if 'border' not in js: - js['border'] = self.border + # Potentially override border settings + if self.border == 0 or self.border == 'none': + js['border'] = '' + elif self.border == 1 or self.border == 'border': + js['border'] = 'border' + elif self.border == 2 or self.border == 'cutmarks': + js['border'] = 'cutmarks' + self.story.append(JinjaFlowable(js, self.staticdir)) if 'forcebreaks' not in js: @@ -334,8 +364,17 @@ class JinjaRenderer(object): if js.get('forcebreaks', False): self.story.append(PageBreak()) + self.js = js + def render(self, output): - doc = SimpleDocTemplate(output, pagesize=self.pagesize, leftMargin=10 * mm, topMargin=5 * mm, rightMargin=10 * mm, bottomMargin=5 * mm) + leftMargin = 10 * mm + topMargin = 5 * mm + if self.js.get('border', None) == 'cutmarks': + cmsize = self.js.get('cutmark_length', DEFAULT_CUTMARK_LENGTH) * mm + self.js.get('cutmark_offset', DEFAULT_CUTMARK_OFS) * mm + topMargin += cmsize + leftMargin += cmsize + + doc = SimpleDocTemplate(output, pagesize=self.pagesize, leftMargin=leftMargin, topMargin=topMargin, rightMargin=10 * mm, bottomMargin=5 * mm) doc.build(self.story) @@ -388,7 +427,7 @@ if __name__ == "__main__": parser.add_argument('attendeelist', type=str, help='JSON file with attendee list') parser.add_argument('outputfile', type=str, help='Name of output PDF file') parser.add_argument('--confjson', type=str, help='JSON representing conference') - parser.add_argument('--borders', action='store_true', help='Enable borders on written file') + parser.add_argument('--borders', choices=['none', 'border', 'cutmarks'], help='Enable borders on written file') parser.add_argument('--pagebreaks', action='store_true', help='Enable pagebreaks on written file') parser.add_argument('--fontroot', type=str, help='fontroot for dejavu fonts') parser.add_argument('--font', type=str, nargs=1, action='append', help=':') diff --git a/postgresqleu/confreg/reports.py b/postgresqleu/confreg/reports.py index 7ad06402..15fd427d 100644 --- a/postgresqleu/confreg/reports.py +++ b/postgresqleu/confreg/reports.py @@ -376,7 +376,7 @@ class ReportWriterPdf(ReportWriterBase): style = [ ("FONTNAME", (0, 0), (-1, -1), "DejaVu Serif"), ] - if self.borders: + if self.borders == 1: style.extend([ ('GRID', (0, 0), (-1, -1), 1, colors.black), ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), @@ -525,7 +525,14 @@ class AttendeeReportManager: format = data['format'] orientation = data['orientation'] pagesize = data.get('pagesize', 'A4') - borders = data.get('border', None) == "on" + borders = int(data.get('border', 0)) + if borders == 2 and format != 'badge': + return HttpResponse("Cutmarks can only be used for badges", content_type='text/plain') + + # Default borders to on for non-badges (for badges, default is read from inside the badge) + if borders == -1 and format != 'badge': + borders = 1 + pagebreaks = data.get('pagebreaks', None) == 'on' extracols = [_f for _f in [x.strip() for x in data['additionalcols'].split(',')] if _f] ofields = [self.fieldmap[f] for f in (data['orderby1'], data['orderby2'])] diff --git a/template/confreg/reports.html b/template/confreg/reports.html index 82067ea4..39d49f34 100644 --- a/template/confreg/reports.html +++ b/template/confreg/reports.html @@ -138,10 +138,18 @@ input#dlgSelectFieldCountText { - Borders - include table and badge borders + PDF Borders: + + + + - Pagebreaks + PDF Pagebreaks: force page break (between badges only) -- 2.39.5