Automatic serial number workflow for paperless-ngx
Posted on Mar. 27, 2025 by Ben Dickson.
Using the following hardware:
- Canon LiDE 300 scanner
- Brother HL-L2305W (mono laser printer)
- Avery L4731-REV25 labels
..and the following software:
- paperless-ngx 2.13.5
- scanbd
- label template using typst
Paperless
I first installed paperless-ngx. I have set things up more or less exactly per it's install docs, the only setting changed are the following env-vars:
PAPERLESS_CONSUMER_ENABLE_BARCODES: "true"
PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE: "true"
PAPERLESS_CONSUMER_BARCODE_SCANNER: "ZXING"
PAPERLESS_CONSUMER_BARCODE_UPSCALE: "1.5"
These are needed to:
- Enable barcodes to read the ASN (automated serial number)
- The ZXING and upscale are needed to read the small (~8x8mm) QR code
The scanner
This scanner I have is very basic USB scanner, with no fancy network connection etc, so I have it connected up to a computer.
The computer is running Proxmox. Getting the USB device to reliably pass to a CT container was a pain, so scanservjs is running in a small VM, with the USB device mapped through (which reliably reconnects via the USB vendor ID - whereas the CT devices seem to require mapping via the /dev/... path which changes on reboot/reconnection)
scanservjs
I initially tried scanservjs, which is a web front-end to the SANE scanner thing. It worked quite well, gave a preview of the document and could easily be configured to push the document straight to paperless.
For reference, the way I set this up:
There is a lot of different ways to get a document into paperless - I used the HTTP API to push the document from the scanning VM to the paperless container.
First I generated an authorization token for paperless-ngx API. This is done by clicking on "My Profile" (top-right of UI, clicking on your username, then generating the key if not already present).
Then in scanservjs config, I add an afterScan callback which sends the document using curl.
/* eslint-disable no-unused-vars */
const options = { paths: ['/usr/lib/scanservjs'] };
const Process = require(require.resolve('./server/classes/process', options));
const dayjs = require(require.resolve('dayjs', options));
module.exports = {
/**
* This method is called after every scan has completed with the resultant
* FileInfo.
* @param {FileInfo} fileInfo
* @returns {Promise.<any>}
*/
async afterScan(fileInfo) {
// Copy the file to the home directory
return await Process.spawn(`curl -H "Authorization: Token TOKENGOESHERE" -F document=@'${fileInfo.fullname}' http://paperless.lan:8000/api/documents/post_document/`);
},
};
Noting TOKENGOESHERE must be changed to the auth token from paperless, and the URL for paperless probably needs to change
Downside to this is you need to put the document in the scanner, open up the browser interface, click scan. I don't have the scanner setup next to the computer, so this was a bit inconvinient.
Scan Button Daemon (scanbd)
scanbd is a small program which listens for the buttons presses from the USB scanner, and runs a command.
I tried this, but configuring this seemed far more complicated than I had the patience for.
Labels
Tried https://github.com/tmaier/asn-qr-code-label-generator - worked okay but hard to tweak the positioning.
Created simple template using Typst (and on Github)
You put the following code in a thing.typ file, then run typst thing.typ and it generates a PDF.
To tweak the positioning, here's what I did:
- change label count to 1 or 2 (so you only print a few boxes per test)
- adjust so boxes line up - units are in printed mm, so you can use physical ruler to measure nudges.
The main bits to tweak are probably the
margin:(overall page margin),label_widthandlabel_height(size of each label), and thecolumn-gutter(spacing between each label) - increase label count by maybe 2, and reprint - to check alignment without wasting complete sheet
- Repeat until good - maybe printing the remainder of a sheet as a final
This way you only waste one sheet to get the alignment perfect
Make sure to put the label in printer in same orientation (for me in the Brother printer, this was with the labels facing down, and the writing upside-down (i.e bottom of text away from me)
#import "@preview/cades:0.3.0": qr-code
#import "@preview/oxifmt:0.2.1": strfmt
#let start_asn = 1
#let label_width = 25.1mm
#let label_height = 10mm
#let qr_size = 8mm
#let codes_per_row = 7
// Function to format number as ASN with leading zeros
#let format_asn(n) = {
strfmt("ASN{:05}", n)
}
#set page(
paper: "a4",
margin: (x: 9mm, y: 15mm)
)
#set table(inset: 0pt, stroke: none)
#table(
columns: codes_per_row,
column-gutter: 2.75mm,
row-gutter: 0mm,
..range(start_asn, start_asn+189).map(num => {
box(
width: label_width,
height: label_height,
stroke: 0.1pt,
{
let asn = format_asn(num)
place(
left,
dx: 1mm,
dy: 1mm,
qr-code(asn, width: qr_size, height: qr_size)
)
place(
center,
dx: 3mm,
dy: 4mm,
text(8pt, asn)
)
}
)
})
)
Note the preview/cades package for typst is a bit slow - I think this is due to it using an embedded Javascript runtime (Jogs) to generate the QR code, which seems a little over complicated way of generating the QR code image. There's another package tiaoma which similarly compiles a C library to wasm and runs that. Not sure why both options go this route instead using a pure-Rust QR code generating crate (there may be some reason I'm overlooking?)