├── .gitignore ├── AveryLabels.py ├── LICENSE ├── README.md ├── docs ├── debug.svg ├── position-helper.svg └── single-label.svg ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | venv 3 | .idea 4 | -------------------------------------------------------------------------------- /AveryLabels.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterator 2 | from reportlab.pdfgen import canvas 3 | from reportlab.lib.pagesizes import A4, letter 4 | from reportlab.lib.units import mm, cm, inch 5 | 6 | # Usage: 7 | # label = AveryLabels.AveryLabel(5160) 8 | # label.open( "labels5160.pdf" ) 9 | # label.render( RenderAddress, 30 ) 10 | # label.close() 11 | # 12 | # 'render' can either pass a callable, which receives the canvas object 13 | # (with X,Y=0,0 at the lower right) or a string "form" name of a form 14 | # previously created with canv.beginForm(). 15 | 16 | 17 | # labels across 18 | # labels down 19 | # label size w/h 20 | # label gutter across/down 21 | # page margins left/top 22 | # page size w/h or name from reportlab.lib.pagesizes 23 | 24 | labelInfo = { 25 | # 22x 32mm x 10mm mini labels 26 | 3044: ( 2, 11, (32, 10), (2,2), (1, 1), (66.5*mm, 120.5*mm)), 27 | 28 | # 189x 25.4mm x 10mm mini labels 29 | 4731: ( 7, 27, (25.4*mm, 10*mm), (2.5*mm, 0), (9*mm, 13.5*mm), A4), 30 | # 2.6 x 1 address labels 31 | 5160: ( 3, 10, (187, 72), (11, 0), (14, 36), A4), 32 | 5161: ( 2, 10, (288, 72), (0, 0), (18, 36), A4), 33 | # 4 x 2 address labels 34 | 5163: ( 2, 5, (288, 144), (0, 0), (18, 36), A4), 35 | # 1.75 x 0.5 return address labels 36 | 5167: ( 4, 20, (126, 36), (0, 0), (54, 36), A4), 37 | # 3.5 x 2 business cards 38 | 5371: ( 2, 5, (252, 144), (0, 0), (54, 36), A4), 39 | 40 | # 48x 45.7x21.2mm 41 | 4778: (4, 12, (45.7*mm, 21.2*mm), (0.25*cm, 0), (1.1*cm, 2*cm), A4), 42 | 43 | # APLI 100984 40x 52.5x29.7mm 44 | 100984: (4, 10, (52.5*mm, 29.7*mm), (0, 0), (0, 0), A4), 45 | 46 | # Royal Green 1x0.375 (1138) 47 | 1138: (7, 22, (1*inch, 0.375*inch), (0.1*inch,0.098*inch), (0.4375*inch, 0.24*inch), letter), 48 | } 49 | 50 | class AveryLabel: 51 | 52 | def __init__(self, label, **kwargs): 53 | data = labelInfo[label] 54 | self.across = data[0] 55 | self.down = data[1] 56 | self.size = data[2] 57 | self.labelsep = self.size[0]+data[3][0], self.size[1]+data[3][1] 58 | self.margins = data[4] 59 | self.topDown = True 60 | self.debug = False 61 | self.pagesize = data[5] 62 | self.position = 0 63 | self.__dict__.update(kwargs) 64 | 65 | def open(self, filename): 66 | self.canvas = canvas.Canvas( filename, pagesize=self.pagesize ) 67 | if self.debug: 68 | self.canvas.setPageCompression( 0 ) 69 | self.canvas.setLineJoin(1) 70 | self.canvas.setLineCap(1) 71 | 72 | def topLeft(self, x=None, y=None): 73 | if x is None: 74 | x = self.position 75 | if y is None: 76 | if self.topDown: 77 | x,y = divmod(x, self.down) 78 | else: 79 | y,x = divmod(x, self.across) 80 | 81 | return ( 82 | self.margins[0]+x*self.labelsep[0], 83 | self.pagesize[1] - self.margins[1] - (y+1)*self.labelsep[1] 84 | ) 85 | 86 | def advance(self): 87 | self.position += 1 88 | if self.position == self.across * self.down: 89 | self.canvas.showPage() 90 | self.position = 0 91 | 92 | def close(self): 93 | if self.position: 94 | self.canvas.showPage() 95 | self.canvas.save() 96 | self.canvas = None 97 | 98 | # To render, you can either create a template and tell me 99 | # "go draw N of these templates" or provide a callback. 100 | # Callback receives canvas, width, height. 101 | # 102 | # Or, pass a callable and an iterator. We'll do one label 103 | # per iteration of the iterator. 104 | 105 | def render( self, thing, count, offset=0, *args ): 106 | assert callable(thing) or isinstance(thing, str) 107 | if isinstance(count, Iterator): 108 | return self.render_iterator( thing, count ) 109 | 110 | canv = self.canvas 111 | for i in range(offset+count): 112 | if i >= offset: 113 | canv.saveState() 114 | canv.translate( *self.topLeft() ) 115 | if self.debug: 116 | canv.setLineWidth( 0.25 ) 117 | canv.rect( 0, 0, self.size[0], self.size[1] ) 118 | if callable(thing): 119 | thing( canv, self.size[0], self.size[1], *args ) 120 | elif isinstance(thing, str): 121 | canv.doForm(thing) 122 | canv.restoreState() 123 | self.advance() 124 | 125 | def render_iterator( self, func, iterator ): 126 | canv = self.canvas 127 | for chunk in iterator: 128 | canv.saveState() 129 | canv.translate( *self.topLeft() ) 130 | if self.debug: 131 | canv.setLineWidth( 0.25 ) 132 | canv.rect( 0, 0, self.size[0], self.size[1] ) 133 | func( canv, self.size[0], self.size[1], chunk ) 134 | canv.restoreState() 135 | self.advance() 136 | 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASN labels for Paperless-ngx on Avery labels 2 | 3 | The [recommended workflow](https://docs.paperless-ngx.com/usage/#usage-recommended-workflow) of [Paperless-ngx](https://docs.paperless-ngx.com/) uses QR codes for ASN (archive serial number) labels. 4 | 5 | ![Single label](docs/single-label.svg) 6 | 7 | This script helps creating them using Python. It outputs a PDF for printing on the label sheets. Make sure to set print size to 100%, not _fit to page_ or similar. 8 | 9 | Other Avery (or competitor's) label sizes can be added to `labelInfo` in `AveryLabels.py`. All other settings are configured at the top part of `main.py`. 10 | 11 | Use these settings for an initial position test to align your printer: 12 | 13 | ```python 14 | mode = "text" 15 | debug = True 16 | 17 | labelsAlreadyPrinted = 0 18 | labelsCorrupted = 0 19 | labelsToPrint = 1 20 | 21 | positionHelper = True 22 | ``` 23 | 24 | ## Settings 25 | 26 | Configurations available in `main.py`: 27 | 28 | * `labelForm`: Select your label sheet version from the `labelInfo` list in `AveryLabels.py`. 29 | * `mode`: 30 | * `qr`: Print a QR code and a text next to it showing the ASN. 31 | * `text`: Print a free text on the label. 32 | * `text`: The free text used for `mode = text`. 33 | * `subLabelsX`/`subLabelsX`: Print multiple labels on a single cutout of a label sheet. 34 | This is helpful for small label sizes even though your physical label cutout sizes are bigger. 35 | Use a steel ruler and a sharp box cutter knife after printing to split up the labels. 36 | * `firstASNOnSheet`: The first ASN number that is/has been printed on the current sheet. Set to `1` on your initial sheet. 37 | * `labelsAlreadyPrinted`: If you have already printed a some labels on the current sheet, use this to skip overwriting them. 38 | * `labelsCorrupted`: Due to misprints or other reason, some labels on the current sheet might have gotten corrupted. This skips the unusable labels. 39 | * `labelsToPrint`: How many labels you want to print now. 40 | 41 | More settings: 42 | * `debug`: For initial calibration, a solid border line will be printed that should line up with the cutouts on your sheet. In `mode = text`, label text will be ascending numbers. 43 | 44 | ![debug.svg](docs/debug.svg) 45 | 46 | * `positionHelper`: Small dots will be printed in the corner of (sub-)labels. Useful as subtle sublabel cutting guides. 47 | 48 | ![position-helper.svg](docs/position-helper.svg) 49 | 50 | # Credits 51 | 52 | This is based on the [work from timrprobocom](https://gist.github.com/timrprobocom/3946aca8ab75df8267bbf892a427a1b7) -------------------------------------------------------------------------------- /docs/debug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | 23 | 24 | 27 | 30 | 34 | 39 | 44 | 49 | 54 | 58 | 63 | 68 | 73 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /docs/position-helper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 17 | 24 | 25 | 28 | 31 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 107 | 111 | 115 | 119 | 123 | 127 | 131 | 135 | 139 | 143 | 147 | 151 | 155 | 159 | 163 | 167 | 171 | 175 | 179 | 183 | 187 | 191 | 195 | 199 | 203 | 207 | 211 | 215 | 219 | 223 | 227 | 231 | 235 | 239 | 243 | 247 | 251 | 255 | 259 | 263 | 267 | 271 | 275 | 279 | 283 | 287 | 291 | 295 | 299 | 303 | 307 | 311 | 315 | 319 | 323 | 327 | 331 | 335 | 339 | 343 | 347 | 351 | 355 | 359 | 363 | 367 | 371 | 375 | 379 | 383 | 387 | 391 | 395 | 399 | 403 | 407 | 411 | 415 | 419 | 423 | 427 | 431 | 435 | 439 | 443 | 447 | 451 | 455 | 459 | 463 | 467 | 471 | 475 | 479 | 483 | 487 | 491 | 495 | 499 | 503 | 507 | 511 | 515 | 519 | 523 | 527 | 531 | 535 | 539 | 543 | 547 | 551 | 555 | 559 | 563 | 567 | 571 | 575 | 579 | 583 | 587 | 591 | 595 | 599 | 603 | 607 | 611 | 615 | 619 | 623 | 627 | 631 | 635 | 639 | 643 | 647 | 651 | 655 | 659 | 663 | 667 | 671 | 675 | 679 | 683 | 687 | 691 | 695 | 699 | 703 | 707 | 711 | 715 | 719 | 723 | 727 | 731 | 735 | 739 | 743 | 747 | 751 | 755 | 759 | 763 | 767 | 771 | 775 | 779 | 783 | 787 | 791 | 795 | 799 | 803 | 807 | 811 | 815 | 819 | 823 | 827 | 831 | 835 | 839 | 843 | 847 | 851 | 855 | 859 | 863 | 867 | 871 | 875 | 879 | 883 | 887 | 891 | 895 | 899 | 903 | 907 | 911 | 915 | 919 | 923 | 927 | 931 | 935 | 939 | 943 | 947 | 951 | 955 | 959 | 963 | 967 | 971 | 976 | 980 | 984 | 988 | 992 | 996 | 1000 | 1001 | 1002 | 1003 | -------------------------------------------------------------------------------- /docs/single-label.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 16 | 18 | 25 | 26 | 29 | 33 | 34 | 36 | 37 | 39 | 41 | 42 | 44 | 46 | 48 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import AveryLabels 5 | from reportlab.lib.units import mm 6 | from reportlab_qrcode import QRCodeImage 7 | from reportlab.pdfgen import canvas 8 | 9 | ### config ### 10 | labelForm = 4778 11 | 12 | # mode "qr" prints a QR code and an ASN (archive serial number) text 13 | mode = "qr" 14 | 15 | # mode text prints a free text 16 | #mode = "text" 17 | #text="6y" 18 | 19 | # print multiple labels on a single cutout of a label sheet 20 | subLabelsX = 2 21 | subLabelsY = 2 22 | 23 | # what was the first ASN number printed on this sheet 24 | firstASNOnSheet = 42 25 | # how many labels have already been printed on this sheet successfully 26 | labelsAlreadyPrinted = 20 27 | # how many labels have been corrupted on this sheet because of misprints 28 | labelsCorrupted = 4 29 | # how many labels should be printed now 30 | labelsToPrint = 18 31 | 32 | fontSize = 2*mm 33 | qrSize = 0.9 34 | qrMargin = 1*mm 35 | 36 | debug = False 37 | positionHelper = True 38 | 39 | ### pre-calculation ### 40 | asnsAlreadyPrinted = (labelsAlreadyPrinted-labelsCorrupted)*subLabelsX*subLabelsY 41 | startASN = firstASNOnSheet + asnsAlreadyPrinted 42 | offsetLabels = labelsAlreadyPrinted+labelsCorrupted 43 | 44 | ### globals ### 45 | currentASN = startASN 46 | 47 | # debug 48 | count = 0 49 | 50 | 51 | def render(c: canvas.Canvas, width: float, height: float): 52 | global currentASN 53 | global subLabelsX 54 | global subLabelsY 55 | 56 | subLabelWidth = width/subLabelsX 57 | subLabelHeight = height/subLabelsY 58 | 59 | for i in range(subLabelsX): 60 | for j in range(subLabelsY-1, -1, -1): # no idea why inverted... 61 | subX = subLabelWidth*i 62 | subY = subLabelHeight*j 63 | 64 | c.saveState() 65 | c.translate(subX, subY) 66 | 67 | if mode == "qr": 68 | barcode_value = f"ASN{currentASN:05d}" 69 | currentASN = currentASN + 1 70 | 71 | qr = QRCodeImage(barcode_value, size=subLabelHeight*qrSize) 72 | qr.drawOn(c, x=qrMargin, y=subLabelHeight*((1-qrSize)/2)) 73 | c.setFont("Helvetica", size=fontSize) 74 | c.drawString(x=subLabelHeight, y=( 75 | subLabelHeight-fontSize)/2, text=barcode_value) 76 | 77 | elif mode == "text": 78 | if debug: 79 | global count 80 | count = count + 1 81 | 82 | c.drawString( 83 | x=(subLabelWidth-2*fontSize)/2, y=(subLabelHeight-fontSize)/2, 84 | text=text if not debug else str(count) 85 | ) 86 | 87 | if positionHelper: 88 | r = 0.1 89 | d = 0 90 | if debug: 91 | r = 0.5 92 | d = r 93 | c.circle(x_cen=0+d, y_cen=0+d, r=r, stroke=1) 94 | c.circle(x_cen=subLabelWidth-d, y_cen=0+d, r=r, stroke=1) 95 | c.circle(x_cen=0+d, y_cen=subLabelHeight-d, r=r, stroke=1) 96 | c.circle(x_cen=subLabelWidth-d, 97 | y_cen=subLabelHeight-d, r=r, stroke=1) 98 | 99 | c.restoreState() 100 | 101 | 102 | outputDirectory = 'out' 103 | fileName = os.path.join(outputDirectory, f"labels-{labelForm}-{mode}.pdf") 104 | 105 | label = AveryLabels.AveryLabel(labelForm) 106 | label.debug = debug 107 | try: 108 | os.makedirs(outputDirectory, exist_ok=True) 109 | except OSError as oe: 110 | sys.exit(f"Failed to create directory '{outputDirectory}': {oe}") 111 | label.open(fileName) 112 | label.render(render, count=labelsToPrint, offset=offsetLabels) 113 | label.close() 114 | 115 | print 116 | print(fileName) 117 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | reportlab 2 | reportlab_qrcode 3 | --------------------------------------------------------------------------------