commit aeb5ca7b3d0df15668727f52ab8f8a18a256e105 Author: gitolicious <26963495+gitolicious@users.noreply.github.com> Date: Sun Oct 29 12:59:58 2023 +0000 feat: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/AveryLabels.py b/AveryLabels.py new file mode 100644 index 0000000..16ab741 --- /dev/null +++ b/AveryLabels.py @@ -0,0 +1,126 @@ +import os +from collections.abc import Iterator +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import LETTER, landscape +from reportlab.lib.units import inch, mm, cm +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + +# Usage: +# label = AveryLabels.AveryLabel(5160) +# label.open( "labels5160.pdf" ) +# label.render( RenderAddress, 30 ) +# label.close() +# +# 'render' can either pass a callable, which receives the canvas object +# (with X,Y=0,0 at the lower right) or a string "form" name of a form +# previously created with canv.beginForm(). + + +# labels across +# labels down +# label size w/h +# label gutter across/down +# page margins left/top + +labelInfo = { + # 2.6 x 1 address labels + 5160: ( 3, 10, (187, 72), (11, 0), (14, 36)), + 5161: ( 2, 10, (288, 72), (0, 0), (18, 36)), + # 4 x 2 address labels + 5163: ( 2, 5, (288, 144), (0, 0), (18, 36)), + # 1.75 x 0.5 return address labels + 5167: ( 4, 20, (126, 36), (0, 0), (54, 36)), + # 3.5 x 2 business cards + 5371: ( 2, 5, (252, 144), (0, 0), (54, 36)), + + # 48x 45.7x21.2mm + 4778: (4, 12, (45.7*mm, 21.2*mm), (0.25*cm, 0), (1.1*cm, 2*cm) ), +} + +class AveryLabel: + + def __init__(self, label, **kwargs): + data = labelInfo[label] + self.across = data[0] + self.down = data[1] + self.size = data[2] + self.labelsep = self.size[0]+data[3][0], self.size[1]+data[3][1] + self.margins = data[4] + self.topDown = True + self.debug = False + self.pagesize = LETTER + self.position = 0 + self.__dict__.update(kwargs) + + def open(self, filename): + self.canvas = canvas.Canvas( filename, pagesize=self.pagesize ) + if self.debug: + self.canvas.setPageCompression( 0 ) + self.canvas.setLineJoin(1) + self.canvas.setLineCap(1) + + def topLeft(self, x=None, y=None): + if x == None: + x = self.position + if y == None: + if self.topDown: + x,y = divmod(x, self.down) + else: + y,x = divmod(x, self.across) + + return ( + self.margins[0]+x*self.labelsep[0], + self.pagesize[1] - self.margins[1] - (y+1)*self.labelsep[1] + ) + + def advance(self): + self.position += 1 + if self.position == self.across * self.down: + self.canvas.showPage() + self.position = 0 + + def close(self): + if self.position: + self.canvas.showPage() + self.canvas.save() + self.canvas = None + + # To render, you can either create a template and tell me + # "go draw N of these templates" or provide a callback. + # Callback receives canvas, width, height. + # + # Or, pass a callable and an iterator. We'll do one label + # per iteration of the iterator. + + def render( self, thing, count, offset=0, *args ): + assert callable(thing) or isinstance(thing, str) + if isinstance(count, Iterator): + return self.render_iterator( thing, count ) + + canv = self.canvas + for i in range(offset+count): + if i >= offset: + canv.saveState() + canv.translate( *self.topLeft() ) + if self.debug: + canv.setLineWidth( 0.25 ) + canv.rect( 0, 0, self.size[0], self.size[1] ) + if callable(thing): + thing( canv, self.size[0], self.size[1], *args ) + elif isinstance(thing, str): + canv.doForm(thing) + canv.restoreState() + self.advance() + + def render_iterator( self, func, iterator ): + canv = self.canvas + for chunk in iterator: + canv.saveState() + canv.translate( *self.topLeft() ) + if self.debug: + canv.setLineWidth( 0.25 ) + canv.rect( 0, 0, self.size[0], self.size[1] ) + func( canv, self.size[0], self.size[1], chunk ) + canv.restoreState() + self.advance() \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1625c17 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b46c30a --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# ASN labels for Paperless-ngx on Avery labels + +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. 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. + +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`. + +Use these settings for an initial position test to align your printer: + +```python +mode = "text" +debug = True + +labelsAlreadyPrinted = 0 +labelsCorrupted = 0 +labelsToPrint = 1 + +positionHelper = True +``` + +# Credits + +This is based on the [work from timrprobocom](https://gist.github.com/timrprobocom/3946aca8ab75df8267bbf892a427a1b7) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..7edf13c --- /dev/null +++ b/main.py @@ -0,0 +1,108 @@ +import AveryLabels +from reportlab.lib.units import mm, cm +from reportlab_qrcode import QRCodeImage +from reportlab.pdfgen import canvas + +### config ### +labelForm = 4778 + +# mode "qr" prints a QR code and an ASN (archive serial number) text +mode = "qr" +subLabelsX = 2 +subLabelsY = 2 + +# mode text prints a free text +#mode = "text" +#text="6y" +#subLabelsX = 5 +#subLabelsY = 3 + +# what was the first ASN number printed on this sheet +firstASNOnSheet = 42 +# how many labels have already been printed on this sheet successfully +labelsAlreadyPrinted = 20 +# how many labels have been corrupted on this sheet because of misprints +labelsCorrupted = 4 +# how many labels should be printed now +labelsToPrint = 18 + +fontSize = 2*mm +qrSize = 0.9 +qrMargin = 1*mm + +debug = False +positionHelper = True + +### pre-calculation ### +asnsAlreadyPrinted = (labelsAlreadyPrinted-labelsCorrupted)*subLabelsX*subLabelsY +startASN = firstASNOnSheet + asnsAlreadyPrinted +offsetLabels = labelsAlreadyPrinted+labelsCorrupted + +### globals ### +currentASN = startASN + +# debug +count = 0 + + +def render(c: canvas.Canvas, width: float, height: float): + global currentASN + global subLabelsX + global subLabelsY + + subLabelWidth = width/subLabelsX + subLabelHeight = height/subLabelsY + + for i in range(subLabelsX): + for j in range(subLabelsY-1, -1, -1): # no idea why inverted... + subX = subLabelWidth*i + subY = subLabelHeight*j + + c.saveState() + c.translate(subX, subY) + + if mode == "qr": + barcode_value = f"ASN{currentASN:05d}" + currentASN = currentASN + 1 + + qr = QRCodeImage(barcode_value, size=subLabelHeight*qrSize) + qr.drawOn(c, x=qrMargin, y=subLabelHeight*((1-qrSize)/2)) + c.setFont("Helvetica", size=fontSize) + c.drawString(x=subLabelHeight, y=( + subLabelHeight-fontSize)/2, text=barcode_value) + + elif mode == "text": + if debug: + global count + count = count + 1 + + c.drawString( + x=(subLabelWidth-2*fontSize)/2, y=(subLabelHeight-fontSize)/2, + text=text if not debug else str(count) + ) + + if positionHelper: + r = 0.1 + d = 0 + if debug: + r = 0.5 + d = r + c.circle(x_cen=0+d, y_cen=0+d, r=r, stroke=1) + c.circle(x_cen=subLabelWidth-d, y_cen=0+d, r=r, stroke=1) + c.circle(x_cen=0+d, y_cen=subLabelHeight-d, r=r, stroke=1) + c.circle(x_cen=subLabelWidth-d, + y_cen=subLabelHeight-d, r=r, stroke=1) + + c.restoreState() + + +fileName = "out/labels-" + str(labelForm) + "-" + str(mode) + ".pdf" + +label = AveryLabels.AveryLabel(labelForm) +label.debug = debug +label.open(fileName) +label.render(render, count=labelsToPrint, offset=offsetLabels) +label.close() + +print +print(fileName) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fcfb494 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +reportlab +reportlab_qrcode