Skip to content

Commit

Permalink
Merge pull request #128 from sbs20/staging
Browse files Browse the repository at this point in the history
Collation, inline filters, OCR fix
  • Loading branch information
sbs20 authored Feb 14, 2021
2 parents 10330ab + 61b1b65 commit ad56a3a
Show file tree
Hide file tree
Showing 19 changed files with 275 additions and 66 deletions.
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "scanservjs-server",
"version": "2.5.0",
"version": "2.6.0",
"description": "scanservjs is a simple web-based UI for SANE which allows you to share a scanner on a network without the need for drivers or complicated installation. scanserv does not do image conversion or manipulation (beyond the bare minimum necessary for the purposes of browser preview) or OCR.",
"scripts": {
"serve": "nodemon --exec 'vue-cli-service serve'",
Expand Down
14 changes: 12 additions & 2 deletions server/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Config = require('./config');
const Context = require('./context');
const Devices = require('./devices');
const FileInfo = require('./file-info');
const Filters = require('./filters');
const Process = require('./process');
const Request = require('./request');
const Scanimage = require('./scanimage');
Expand Down Expand Up @@ -62,15 +63,22 @@ class Api {
}

/**
* @param {string[]} filters
* @returns {Promise.<Buffer>}
*/
static async readPreview() {
static async readPreview(filters) {
// The UI relies on this image being the correct aspect ratio. If there is a
// preview image then just use it.
const source = new FileInfo(`${Config.previewDirectory}preview.tif`);
if (source.exists()) {
const buffer = source.toBuffer();
return await Process.chain(Config.previewPipeline.commands, buffer, { ignoreErrors: true });
const cmds = [...Config.previewPipeline.commands];
if (filters) {
const params = Filters.build(filters, true);
cmds.splice(0, 0, `convert - ${params} tif:-`);
}

return await Process.chain(cmds, buffer, { ignoreErrors: true });
}

// If not then it's possible the default image is not quite the correct aspect ratio
Expand Down Expand Up @@ -104,6 +112,8 @@ class Api {
preview.delete();
}
const context = await Context.create();
context.filters = context.filters.map(f => f.description);
context.pipelines = context.pipelines.map(p => p.description);
return context;
}
}
Expand Down
22 changes: 20 additions & 2 deletions server/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ class Config {
'convert - -quality 75 jpg:-'
]
},

filters: [
{
description: 'Auto level',
params: '-auto-level'
},
{
description: 'Threshold',
params: '-channel RGB -threshold 80%'
},
{
description: 'Blur',
params: '-blur 1'
}
],

pipelines: [
{
Expand Down Expand Up @@ -157,15 +172,15 @@ class Config {
description: 'OCR | PDF (JPG | High quality)',
commands: [
'convert @- -quality 92 tmp-%d.jpg && ls tmp-*.jpg',
`${Config.tesseract} -l ${Config.ocrLanguage} -c stream_filelist=true - - pdf > scan-0001.pdf`,
`${this.tesseract} -l ${this.ocrLanguage} -c stream_filelist=true - - pdf > scan-0001.pdf`,
'ls scan-*.*'
]
},
{
extension: 'txt',
description: 'OCR | Text file',
commands: [
`${Config.tesseract} -l ${Config.ocrLanguage} -c stream_filelist=true - - txt > scan-0001.txt`,
`${this.tesseract} -l ${this.ocrLanguage} -c stream_filelist=true - - txt > scan-0001.txt`,
'ls scan-*.*'
]
}
Expand Down Expand Up @@ -213,6 +228,9 @@ class Config {
}
}

/**
* @returns {Configuration}
*/
static instance() {
if (instance === null) {
instance = new Config();
Expand Down
28 changes: 14 additions & 14 deletions server/src/configure.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,46 +66,47 @@ module.exports = (app, rootPath) => {

app.use(bodyParser.json());

app.get('/files', async (req, res) => {
app.get(['/context', '/context/:force'], async (req, res) => {
logRequest(req);
const force = req.params.force && req.params.force === 'force';
try {
res.send(await Api.fileList());
res.send(await Api.context(force));
} catch (error) {
sendError(res, 500, error);
}
});

app.get('/files/*', (req, res) => {
app.get('/files', async (req, res) => {
logRequest(req);
try {
res.download(req.params[0]);
res.send(await Api.fileList());
} catch (error) {
sendError(res, 500, error);
}
});

app.delete('/files/*', (req, res) => {
app.get('/files/*', (req, res) => {
logRequest(req);
try {
res.send(Api.fileDelete(req.params[0]));
res.download(req.params[0]);
} catch (error) {
sendError(res, 500, error);
}
});

app.post('/scan', async (req, res) => {
app.delete('/files/*', (req, res) => {
logRequest(req);
try {
res.send(await Api.scan(req.body));
res.send(Api.fileDelete(req.params[0]));
} catch (error) {
sendError(res, 500, error);
}
});

app.get('/preview', async (req, res) => {
app.post('/preview', async (req, res) => {
logRequest(req);
try {
const buffer = await Api.readPreview();
const buffer = await Api.readPreview(req.body);
res.send({
content: buffer.toString('base64')
});
Expand All @@ -114,7 +115,7 @@ module.exports = (app, rootPath) => {
}
});

app.post('/preview', async (req, res) => {
app.post('/scanner/preview', async (req, res) => {
logRequest(req);
try {
res.send(await Api.createPreview(req.body));
Expand All @@ -123,11 +124,10 @@ module.exports = (app, rootPath) => {
}
});

app.get(['/context', '/context/:force'], async (req, res) => {
app.post('/scanner/scan', async (req, res) => {
logRequest(req);
const force = req.params.force && req.params.force === 'force';
try {
res.send(await Api.context(force));
res.send(await Api.scan(req.body));
} catch (error) {
sendError(res, 500, error);
}
Expand Down
5 changes: 3 additions & 2 deletions server/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
BATCH_NONE: 'none',
BATCH_MANUAL: 'manual',
BATCH_AUTO: 'auto',
BATCH_AUTO_COLLATE: 'auto-collate',
TEMP_FILESTEM: '~tmp-scan-'
BATCH_COLLATE_STANDARD: 'auto-collate-standard',
BATCH_COLLATE_REVERSE: 'auto-collate-reverse',
TEMP_FILESTEM: '~tmp-scan'
};
3 changes: 3 additions & 0 deletions server/src/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class Context {
];
/** @type {Pipeline[]} */
this.pipelines = Config.pipelines;

/** @type {Filter[]} */
this.filters = Config.filters;
}

/**
Expand Down
21 changes: 21 additions & 0 deletions server/src/filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const Config = require('./config');

class Filters {
/**
* @param {string[]} selected
* @param {boolean} [ignoreRotations]
* @returns {string}
*/
static build(selected, ignoreRotations) {
ignoreRotations = ignoreRotations === undefined ? false : ignoreRotations;
const filters = Config.filters
.filter(f => !ignoreRotations || !f.params.includes('-rotate'))
.filter(f => selected.includes(f.description));

const params = filters.map(f => f.params).join(' ');
return params;
}

}

module.exports = Filters;
3 changes: 2 additions & 1 deletion server/src/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Request {
mode: data.params.mode || features['--mode'].default,
format: 'tiff'
},
filters: data.filters || [],
pipeline: data.pipeline || null,
batch: data.batch || Constants.BATCH_NONE,
index: data.index || 1
Expand All @@ -54,7 +55,7 @@ class Request {
: true;
}

log.debug(JSON.stringify(this));
log.trace(JSON.stringify(this));
return this;
}
}
Expand Down
21 changes: 18 additions & 3 deletions server/src/scan-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const Config = require('./config');
const Constants = require('./constants');
const Context = require('./context');
const FileInfo = require('./file-info');
const Filters = require('./filters');
const Process = require('./process');
const Request = require('./request');
const Scanimage = require('./scanimage');
Expand All @@ -14,7 +15,7 @@ class ScanController {
/** @type {Context} */
this.context = null;

/** @type {Request} */
/** @type {ScanRequest} */
this.request = null;

this.dir = FileInfo.create(Config.tempDirectory);
Expand All @@ -37,7 +38,8 @@ class ScanController {
this.performScan = this.request.index > 0;
this.finishUp = [Constants.BATCH_AUTO, Constants.BATCH_NONE].includes(this.request.batch)
|| (this.request.batch === Constants.BATCH_MANUAL && this.request.index < 0)
|| (this.request.batch === Constants.BATCH_AUTO_COLLATE && this.request.index === 2);
|| (this.request.batch === Constants.BATCH_COLLATE_STANDARD && this.request.index === 2)
|| (this.request.batch === Constants.BATCH_COLLATE_REVERSE && this.request.index === 2);
}

/**
Expand Down Expand Up @@ -70,9 +72,22 @@ class ScanController {
log.debug(`Post processing: ${this.pipeline.description}`);
let files = (await this.listFiles()).filter(f => f.extension === '.tif');

// Update preview with the first image
// Update preview with the first image (pre filter)
await this.updatePreview(files[0].name);

// Collation
if ([Constants.BATCH_COLLATE_STANDARD, Constants.BATCH_COLLATE_REVERSE].includes(this.request.batch)) {
files = Util.collate(files, this.request.batch === Constants.BATCH_COLLATE_STANDARD);
}

// Apply filters
if (this.request.filters.length > 0) {
const stdin = files.map(f => f.name).join('\n');
const cmd = `convert @- ${Filters.build(this.request.filters)} f-%04d.tif`;
await Process.spawn(cmd, stdin, { cwd: Config.tempDirectory });
files = (await this.listFiles()).filter(f => f.name.match(/f-\d{4}\.tif/));
}

const stdin = files.map(f => f.name).join('\n');
log.debug('Executing cmds:', this.pipeline.commands);
const stdout = await Process.chain(this.pipeline.commands, stdin, { cwd: Config.tempDirectory });
Expand Down
7 changes: 4 additions & 3 deletions server/src/scanimage.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Scanimage {
*/
static filename(page) {
const number = `000${page}`.slice(-4);
return `${Config.tempDirectory}${Constants.TEMP_FILESTEM}0-${number}.tif`;
return `${Config.tempDirectory}${Constants.TEMP_FILESTEM}-0-${number}.tif`;
}

/**
Expand Down Expand Up @@ -69,8 +69,9 @@ class Scanimage {
if (params.mode === 'Lineart' && params.dynamicLineart === false) {
cmdBuilder.arg('--disable-dynamic-lineart=yes');
}
if ([Constants.BATCH_AUTO, Constants.BATCH_AUTO_COLLATE].includes(request.batch)) {
const pattern = `${Config.tempDirectory}${Constants.TEMP_FILESTEM}%04d-${request.index}.tif`;
if ([Constants.BATCH_AUTO, Constants.BATCH_COLLATE_STANDARD, Constants.BATCH_COLLATE_REVERSE]
.includes(request.batch)) {
const pattern = `${Config.tempDirectory}${Constants.TEMP_FILESTEM}-${request.index}-%04d.tif`;
cmdBuilder.arg(`--batch=${pattern}`);
} else {
cmdBuilder.arg(`> ${Scanimage.filename(request.index)}`);
Expand Down
27 changes: 27 additions & 0 deletions server/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,39 @@
* @property {Object.<string, ScanDeviceFeature>} features
*/

/**
* @typedef {Object} Filter
* @property {string} description
* @property {string} params
*/

/**
* @typedef {Object} Pipeline
* @property {string} extension
* @property {string} description
* @property {string[]} commands
*/

/**
* @typedef {Object} Configuration
* @property {string} version
* @property {number} port
* @property {ScanDevice} devices
* @property {string} ocrLanguage
* @property {string} scanimage
* @property {string} convert
* @property {string} tesseract
* @property {boolean} allowUnsafePaths
* @property {string} devicesPath
* @property {string} outputDirectory
* @property {string} previewDirectory
* @property {string} tempDirectory
* @property {number} previewResolution
* @property {Pipeline} previewPipeline
* @property {Filter[]} filters
* @property {Pipeline[]} pipelines
*/

/**
* @typedef {Object} ScanRequestParameters
* @property {string} deviceId
Expand All @@ -53,6 +79,7 @@
/**
* @typedef {Object} ScanRequest
* @property {ScanRequestParameters} params
* @property {string[]} filters
* @property {string} pipeline
* @property {string} batch
* @property {number} index
Expand Down
27 changes: 27 additions & 0 deletions server/src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,33 @@ const Util = {
zip.addLocalFile(filepath);
}
zip.writeZip(destination);
},

/**
* @param {FileInfo[]} files
* @param {boolean} standard
* @returns {FileInfo[]}
*/
collate(files, standard) {
const odd = files.filter(f => f.name.match(/-1-\d{4}\.tif/));
const even = files.filter(f => f.name.match(/-2-\d{4}\.tif/));

// This is counter-intuitive and probably badly named. But by default the
// even pages are coming in in reverse order - so that is standard. If the
// scanner has output the scans in the reverse order then we don't need to
// reverse the even pages
if (standard) {
even.sort((f1, f2) => -f1.name.localeCompare(f2.name));
}

files = [];
for (let index = 0; index < odd.length; index++) {
files.push(odd[index]);
if (even[index]) {
files.push(even[index]);
}
}
return files;
}
};

Expand Down
Loading

0 comments on commit ad56a3a

Please sign in to comment.