SANE scanner nodejs web ui
You have an old Canon flatbed but it returns daft default values for width and height. You also want to change the default resolution and limit the resolution options.
/**
* @param {ScanDevice[]} devices
*/
afterDevices(devices) {
// Override the defaults for plustek scanners
devices
.filter(d => d.id.includes('plustek'))
.forEach(device => {
device.features['--resolution'].default = 150;
device.features['--resolution'].options = [75, 150, 300, 600];
device.features['-x'].default = 215;
device.features['-y'].default = 297;
});
}
Some scanners (I’m looking at you, Brother) report their dimensions incorrectly. This throws off the cropping logic because scanservjs incorrectly trusts the SANE output.
afterDevices(devices) {
devices
.filter(d => d.id.includes('brother'))
.forEach(device => {
device.features['-l'].limits = [0, 215];
device.features['-t'].limits = [0, 297];
device.features['-x'].default = 215;
device.features['-x'].limits = [0, 215];
device.features['-y'].default = 297;
device.features['-y'].limits = [0, 297];
});
}
If you have many scanners available then you may wish to give devices friendly
names as per #212.
{ScanDevice}
objects have a name
attribute which defaults to the id
but
can be anything you want it to be. You just need to override it.
afterDevices(devices) {
const deviceNames = {
'plustek:libusb:001:003': 'Downstairs Canon Flatbed',
'test:device:unreal': 'Upstairs Canon MFD'
};
devices
.filter(d => d.id in deviceNames)
.forEach(d => d.name = deviceNames[d.id]);
}
You may wish to add your own custom pipelines. Pipelines are arrays of shell commands which run after scans.
The pipeline commands will be started subsequently but in separate subprocesses. Therefore, e.g., the definition of variables in one line, can’t be accessed in another. If you want full scripting capabilities, create a script externally (e.g. in /usr/local/bin), make it executable, and start it here. The script will be executed from the tmp-folder of scanservjs, thus, it can access all temporary files. Alternatively, a script can also request parameters, which can be set here.
The first command of the pipeline will receive, in its standard input, the newline-separated list of files that have been processed (one file per page). Their order may differ from the alphabetical and numeric order, but it is important to use the file order given, because the collation feature may have re-ordered them (e.g. in case the user scanned every odd, then every even page).
You are free to create whatever kind of files you want, however, the last command of the pipeline needs to return a list of files, which can be further processed by scanservjs to create the final result within scanservjs.
To learn more read the example source. This will insert your own pipelines at the top of the list.
afterConfig(config) {
const pipelines = [
{
extension: 'jpg',
description: 'TEST PIPELINE | Terrible quality',
commands: [
'convert @- -quality 20 scan-%04d.jpg',
'ls scan-*.*'
]
},
{
extension: 'jpg',
description: 'TEST PIPELINE 2 | Silly quality',
commands: [
'convert @- -quality 99 scan-%04d.jpg',
'ls scan-*.*'
]
}
];
config.pipelines.splice(0, 0, ...pipelines);
},
ocrmypdf
ocrmypdf is a tool which deskews crooked scans, automatically fixes incorrectly rotated pages and performs OCR with tesseract. It needs to be installed separately, see the official instructions.
Then, add the following pipeline:
config.pipelines.push({
extension: 'pdf',
description: 'ocrmypdf (JPG | @:pipeline.high-quality)',
get commands() {
return [
'convert @- -quality 92 tmp-%04d.jpg && ls tmp-*.jpg',
'convert @- pdf:-',
`ocrmypdf -l ${config.ocrLanguage} --deskew --rotate-pages - scan_0000.pdf`,
'ls scan_*.*'
];
}
});
const options = { paths: ['/usr/lib/scanservjs'] };
const dayjs = require(require.resolve('dayjs', options));
module.exports = {
afterConfig(config) {
config.filename = () => {
return `my_filestem_${dayjs().format('DD-MM-YYYY HH-mm-ss')}`;
};
config.log.level = 'DEBUG';
}
}
Exercise caution with this recipe - the app is designed not to allow unsafe paths by default. If you are happy to disable this check, then go ahead.
module.exports = {
afterConfig(config) {
// Set your path here
config.outputDirectory = '/home/me/scanned';
}
}
You can use a filter to include only the paper sizes you want. To only show ISO sizes do something like the following. You can obviously extend or reverse the filter as required.
module.exports = {
afterConfig(config) {
config.paperSizes = config.paperSizes.filter(p => /[AB]\d/.test(p.name));
}
}
--page-width
and --page-height
Some scanners need additional arguments for scanimage to behave. By default, the application should just work. While the end user is not presented with an option to change these in the UI, the parameters will be automatically defaulted according to the values presented by SANE - and will do so per device. Should the values not be to your liking then you can override them as per any other device setting:
module.exports = {
afterDevices(devices) {
devices
.filter(d => d.id.includes('fujitsu'))
.forEach(device => {
device.features['--page-height'] = {
default: 297,
limits: [0, 297]
};
device.features['--page-width'] = {
default: 215,
limits: [0, 215]
};
});
}
Devices also have their own batch modes, filters and pipelines. By default, each
device inherits the settings in config
.
device.settings = {
batchMode: {
options: config.batchModes,
default: config.batchModes[0]
},
filters: {
options: config.filters.map(f => f.description),
default: []
},
pipeline: {
options: config.pipelines.map(p => p.description),
default: config.pipelines[0].description
}
};
But it’s possible to override these settings to limit the options available to a specific device or change the default. So just as with other device overrides:
/**
* @param {ScanDevice[]} devices
*/
afterDevices(devices) {
// Override the defaults for plustek scanners
devices
.filter(d => d.id.includes('plustek'))
.forEach(device => {
device.settings.batchMode.options = ['none'];
device.settings.batchMode.default = 'none';
device.settings.filters.default = ['filter.threshold'];
});
}
Create a file action to do whatever you like - this might be useful for
integrating with paperless-ng
. The example below defines a pipeline which
creates a PDF and then copies it to the home directory on completion.
const options = { paths: ['/usr/lib/scanservjs'] };
const Process = require(require.resolve('./server/classes/process', options));
module.exports = {
/**
* @param {Configuration} config
*/
afterConfig(config) {
// Add a custom copy pipeline
config.pipelines.push({
extension: 'pdf',
description: 'PDF to home directory',
commands: [
'convert @- -quality 92 tmp-%04d.jpg && ls tmp-*.jpg',
'convert @- scan-0000.pdf',
'ls scan-*.*'
],
afterAction: 'Copy to Home Directory'
});
},
/**
* @type {Action[]}
*/
actions: [
{
name: 'Copy to Home Directory',
async execute(fileInfo) {
return await Process.spawn(`cp '${fileInfo.fullname}' ~/`);
}
}
]
};
This is not meaningfully secure. There is no transport security by default, and the credentials are stored in plain text. But it may offer some peace of mind to deter casual browsing.
Just populate the config with a dictionary with username keys and password values.
module.exports = {
afterConfig(config) {
config.users = {
'user1': 'password1',
'user2': 'password2'
};
}
}
This recipe covers all major cloud providers such as Amazon, Dropbox, Google (Drive/Photos), Microsoft (Azure Blob Storage, OneDrive), Nextcloud (via WebDav), a network share of your choice (S/FTP) and many more by using Rclone.
Now you have a choice. If you want the update to occur on the scan itself then you need to integrate into the pipeline. Alternatively, sync the output directory itself and use either inotify or cron. If you want to embed into the pipeline then something like the following may help:
/**
* @param {FileInfo} fileInfo
* @returns {Promise.<Buffer>}
*/
async afterScan(fileInfo) {
// Copy the scan to my home directory
return await Process.spawn(`rclone copy '${fileInfo.fullname}' YOUR_PROVIDER:/path/to/folder`);
}
sudo apt install mpack
to send the scanned filesNow create the following pipeline in your config/config.local.js
const options = { paths: ['/usr/lib/scanservjs'] };
const Process = require(require.resolve('./server/classes/process', options));
module.exports = {
afterConfig(config) {
config.pipelines.push({
extension: 'pdf',
description: 'ocrmypdf',
get commands() {
return [
'convert @- -quality 92 tmp-%04d.jpg && ls tmp-*.jpg',
'convert @- pdf:-',
`ocrmypdf -l ${config.ocrLanguage} --deskew --rotate-pages --force-ocr - "scan_0.pdf"`,
'ls scan_*.*'
];
}
});
},
/**
* @param {FileInfo} fileInfo
* @returns {Promise.<Buffer>}
*/
async afterScan(fileInfo) {
return await Process.spawn(`mpack -s "Document from Scanner@Office" "${fileInfo.fullname}" email@address.tld`);
}
};
If you have other recipes then please share them.