├── .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 | 
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 | 
45 |
46 | * `positionHelper`: Small dots will be printed in the corner of (sub-)labels. Useful as subtle sublabel cutting guides.
47 |
48 | 
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 |
81 |
--------------------------------------------------------------------------------
/docs/position-helper.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
1003 |
--------------------------------------------------------------------------------
/docs/single-label.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------