402 lines
13 KiB
TypeScript
402 lines
13 KiB
TypeScript
|
// deno-fmt-ignore-file
|
||
|
// deno-lint-ignore-file
|
||
|
// This code was bundled using `deno bundle` and it's not recommended to edit it manually
|
||
|
|
||
|
class DenoStdInternalError extends Error {
|
||
|
constructor(message){
|
||
|
super(message);
|
||
|
this.name = "DenoStdInternalError";
|
||
|
}
|
||
|
}
|
||
|
function assert(expr, msg = "") {
|
||
|
if (!expr) {
|
||
|
throw new DenoStdInternalError(msg);
|
||
|
}
|
||
|
}
|
||
|
const { hasOwn } = Object;
|
||
|
function get(obj, key) {
|
||
|
if (hasOwn(obj, key)) {
|
||
|
return obj[key];
|
||
|
}
|
||
|
}
|
||
|
function getForce(obj, key) {
|
||
|
const v = get(obj, key);
|
||
|
assert(v != null);
|
||
|
return v;
|
||
|
}
|
||
|
function isNumber(x) {
|
||
|
if (typeof x === "number") return true;
|
||
|
if (/^0x[0-9a-f]+$/i.test(String(x))) return true;
|
||
|
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(String(x));
|
||
|
}
|
||
|
function hasKey(obj, keys) {
|
||
|
let o = obj;
|
||
|
keys.slice(0, -1).forEach((key)=>{
|
||
|
o = get(o, key) ?? {};
|
||
|
});
|
||
|
const key = keys[keys.length - 1];
|
||
|
return hasOwn(o, key);
|
||
|
}
|
||
|
function parse(args, { "--": doubleDash = false , alias ={} , boolean: __boolean = false , default: defaults = {} , stopEarly =false , string =[] , collect =[] , negatable =[] , unknown =(i)=>i } = {}) {
|
||
|
const aliases = {};
|
||
|
const flags = {
|
||
|
bools: {},
|
||
|
strings: {},
|
||
|
unknownFn: unknown,
|
||
|
allBools: false,
|
||
|
collect: {},
|
||
|
negatable: {}
|
||
|
};
|
||
|
if (alias !== undefined) {
|
||
|
for(const key in alias){
|
||
|
const val = getForce(alias, key);
|
||
|
if (typeof val === "string") {
|
||
|
aliases[key] = [
|
||
|
val
|
||
|
];
|
||
|
} else {
|
||
|
aliases[key] = val;
|
||
|
}
|
||
|
for (const alias1 of getForce(aliases, key)){
|
||
|
aliases[alias1] = [
|
||
|
key
|
||
|
].concat(aliases[key].filter((y)=>alias1 !== y));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (__boolean !== undefined) {
|
||
|
if (typeof __boolean === "boolean") {
|
||
|
flags.allBools = !!__boolean;
|
||
|
} else {
|
||
|
const booleanArgs = typeof __boolean === "string" ? [
|
||
|
__boolean
|
||
|
] : __boolean;
|
||
|
for (const key1 of booleanArgs.filter(Boolean)){
|
||
|
flags.bools[key1] = true;
|
||
|
const alias2 = get(aliases, key1);
|
||
|
if (alias2) {
|
||
|
for (const al of alias2){
|
||
|
flags.bools[al] = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (string !== undefined) {
|
||
|
const stringArgs = typeof string === "string" ? [
|
||
|
string
|
||
|
] : string;
|
||
|
for (const key2 of stringArgs.filter(Boolean)){
|
||
|
flags.strings[key2] = true;
|
||
|
const alias3 = get(aliases, key2);
|
||
|
if (alias3) {
|
||
|
for (const al1 of alias3){
|
||
|
flags.strings[al1] = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (collect !== undefined) {
|
||
|
const collectArgs = typeof collect === "string" ? [
|
||
|
collect
|
||
|
] : collect;
|
||
|
for (const key3 of collectArgs.filter(Boolean)){
|
||
|
flags.collect[key3] = true;
|
||
|
const alias4 = get(aliases, key3);
|
||
|
if (alias4) {
|
||
|
for (const al2 of alias4){
|
||
|
flags.collect[al2] = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (negatable !== undefined) {
|
||
|
const negatableArgs = typeof negatable === "string" ? [
|
||
|
negatable
|
||
|
] : negatable;
|
||
|
for (const key4 of negatableArgs.filter(Boolean)){
|
||
|
flags.negatable[key4] = true;
|
||
|
const alias5 = get(aliases, key4);
|
||
|
if (alias5) {
|
||
|
for (const al3 of alias5){
|
||
|
flags.negatable[al3] = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const argv = {
|
||
|
_: []
|
||
|
};
|
||
|
function argDefined(key, arg) {
|
||
|
return flags.allBools && /^--[^=]+$/.test(arg) || get(flags.bools, key) || !!get(flags.strings, key) || !!get(aliases, key);
|
||
|
}
|
||
|
function setKey(obj, name, value, collect = true) {
|
||
|
let o = obj;
|
||
|
const keys = name.split(".");
|
||
|
keys.slice(0, -1).forEach(function(key) {
|
||
|
if (get(o, key) === undefined) {
|
||
|
o[key] = {};
|
||
|
}
|
||
|
o = get(o, key);
|
||
|
});
|
||
|
const key = keys[keys.length - 1];
|
||
|
const collectable = collect && !!get(flags.collect, name);
|
||
|
if (!collectable) {
|
||
|
o[key] = value;
|
||
|
} else if (get(o, key) === undefined) {
|
||
|
o[key] = [
|
||
|
value
|
||
|
];
|
||
|
} else if (Array.isArray(get(o, key))) {
|
||
|
o[key].push(value);
|
||
|
} else {
|
||
|
o[key] = [
|
||
|
get(o, key),
|
||
|
value
|
||
|
];
|
||
|
}
|
||
|
}
|
||
|
function setArg(key, val, arg = undefined, collect) {
|
||
|
if (arg && flags.unknownFn && !argDefined(key, arg)) {
|
||
|
if (flags.unknownFn(arg, key, val) === false) return;
|
||
|
}
|
||
|
const value = !get(flags.strings, key) && isNumber(val) ? Number(val) : val;
|
||
|
setKey(argv, key, value, collect);
|
||
|
const alias = get(aliases, key);
|
||
|
if (alias) {
|
||
|
for (const x of alias){
|
||
|
setKey(argv, x, value, collect);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function aliasIsBoolean(key) {
|
||
|
return getForce(aliases, key).some((x)=>typeof get(flags.bools, x) === "boolean");
|
||
|
}
|
||
|
let notFlags = [];
|
||
|
if (args.includes("--")) {
|
||
|
notFlags = args.slice(args.indexOf("--") + 1);
|
||
|
args = args.slice(0, args.indexOf("--"));
|
||
|
}
|
||
|
for(let i = 0; i < args.length; i++){
|
||
|
const arg = args[i];
|
||
|
if (/^--.+=/.test(arg)) {
|
||
|
const m = arg.match(/^--([^=]+)=(.*)$/s);
|
||
|
assert(m != null);
|
||
|
const [, key5, value] = m;
|
||
|
if (flags.bools[key5]) {
|
||
|
const booleanValue = value !== "false";
|
||
|
setArg(key5, booleanValue, arg);
|
||
|
} else {
|
||
|
setArg(key5, value, arg);
|
||
|
}
|
||
|
} else if (/^--no-.+/.test(arg) && get(flags.negatable, arg.replace(/^--no-/, ""))) {
|
||
|
const m1 = arg.match(/^--no-(.+)/);
|
||
|
assert(m1 != null);
|
||
|
setArg(m1[1], false, arg, false);
|
||
|
} else if (/^--.+/.test(arg)) {
|
||
|
const m2 = arg.match(/^--(.+)/);
|
||
|
assert(m2 != null);
|
||
|
const [, key6] = m2;
|
||
|
const next = args[i + 1];
|
||
|
if (next !== undefined && !/^-/.test(next) && !get(flags.bools, key6) && !flags.allBools && (get(aliases, key6) ? !aliasIsBoolean(key6) : true)) {
|
||
|
setArg(key6, next, arg);
|
||
|
i++;
|
||
|
} else if (/^(true|false)$/.test(next)) {
|
||
|
setArg(key6, next === "true", arg);
|
||
|
i++;
|
||
|
} else {
|
||
|
setArg(key6, get(flags.strings, key6) ? "" : true, arg);
|
||
|
}
|
||
|
} else if (/^-[^-]+/.test(arg)) {
|
||
|
const letters = arg.slice(1, -1).split("");
|
||
|
let broken = false;
|
||
|
for(let j = 0; j < letters.length; j++){
|
||
|
const next1 = arg.slice(j + 2);
|
||
|
if (next1 === "-") {
|
||
|
setArg(letters[j], next1, arg);
|
||
|
continue;
|
||
|
}
|
||
|
if (/[A-Za-z]/.test(letters[j]) && /=/.test(next1)) {
|
||
|
setArg(letters[j], next1.split(/=(.+)/)[1], arg);
|
||
|
broken = true;
|
||
|
break;
|
||
|
}
|
||
|
if (/[A-Za-z]/.test(letters[j]) && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next1)) {
|
||
|
setArg(letters[j], next1, arg);
|
||
|
broken = true;
|
||
|
break;
|
||
|
}
|
||
|
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
|
||
|
setArg(letters[j], arg.slice(j + 2), arg);
|
||
|
broken = true;
|
||
|
break;
|
||
|
} else {
|
||
|
setArg(letters[j], get(flags.strings, letters[j]) ? "" : true, arg);
|
||
|
}
|
||
|
}
|
||
|
const [key7] = arg.slice(-1);
|
||
|
if (!broken && key7 !== "-") {
|
||
|
if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1]) && !get(flags.bools, key7) && (get(aliases, key7) ? !aliasIsBoolean(key7) : true)) {
|
||
|
setArg(key7, args[i + 1], arg);
|
||
|
i++;
|
||
|
} else if (args[i + 1] && /^(true|false)$/.test(args[i + 1])) {
|
||
|
setArg(key7, args[i + 1] === "true", arg);
|
||
|
i++;
|
||
|
} else {
|
||
|
setArg(key7, get(flags.strings, key7) ? "" : true, arg);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
|
||
|
argv._.push(flags.strings["_"] ?? !isNumber(arg) ? arg : Number(arg));
|
||
|
}
|
||
|
if (stopEarly) {
|
||
|
argv._.push(...args.slice(i + 1));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
for (const [key8, value1] of Object.entries(defaults)){
|
||
|
if (!hasKey(argv, key8.split("."))) {
|
||
|
setKey(argv, key8, value1);
|
||
|
if (aliases[key8]) {
|
||
|
for (const x of aliases[key8]){
|
||
|
setKey(argv, x, value1);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
for (const key9 of Object.keys(flags.bools)){
|
||
|
if (!hasKey(argv, key9.split("."))) {
|
||
|
const value2 = get(flags.collect, key9) ? [] : false;
|
||
|
setKey(argv, key9, value2, false);
|
||
|
}
|
||
|
}
|
||
|
for (const key10 of Object.keys(flags.strings)){
|
||
|
if (!hasKey(argv, key10.split(".")) && get(flags.collect, key10)) {
|
||
|
setKey(argv, key10, [], false);
|
||
|
}
|
||
|
}
|
||
|
if (doubleDash) {
|
||
|
argv["--"] = [];
|
||
|
for (const key11 of notFlags){
|
||
|
argv["--"].push(key11);
|
||
|
}
|
||
|
} else {
|
||
|
for (const key12 of notFlags){
|
||
|
argv._.push(key12);
|
||
|
}
|
||
|
}
|
||
|
return argv;
|
||
|
}
|
||
|
const DEFAULT_BUFFER_SIZE = 32 * 1024;
|
||
|
async function* iterateReader(r, options) {
|
||
|
const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
|
||
|
const b = new Uint8Array(bufSize);
|
||
|
while(true){
|
||
|
const result = await r.read(b);
|
||
|
if (result === null) {
|
||
|
break;
|
||
|
}
|
||
|
yield b.slice(0, result);
|
||
|
}
|
||
|
}
|
||
|
const getRandomString = ()=>`${+new Date()}-${Math.round(Math.random() * 100000)}`;
|
||
|
(async ()=>{
|
||
|
const tempDir = await Deno.makeTempDir();
|
||
|
const args = parse(Deno.args);
|
||
|
if (args.help || args.h) {
|
||
|
console.log(`
|
||
|
Usage: scan2paperless [OPTION] [NAME]
|
||
|
|
||
|
Scan one or multiple pages into a PDF document ready for consumption by paperless
|
||
|
|
||
|
NAME will be the name of the document without extension.
|
||
|
|
||
|
Available options:
|
||
|
-d | --device=DEVICE Scanner device to use. Check \`scanimage -L\` for available devices.
|
||
|
This defaults to \$SCANNER_DEFAULT_DEVICE
|
||
|
-o | --output-dir=DIR Directory to write final PDF to. This defaults to \$SCANNER_OUTPUT_DIR
|
||
|
-h | --help show this help message
|
||
|
|
||
|
Example:
|
||
|
|
||
|
scan2paperless \
|
||
|
-d hp3900:libusb:005:002 \
|
||
|
-n 5 \
|
||
|
-o /var/lib/paperless/consume \
|
||
|
bank-statement-01-2023
|
||
|
`);
|
||
|
Deno.exit(0);
|
||
|
}
|
||
|
const device = args.d || args.device || Deno.env.get('SCANNER_DEFAULT_DEVICE');
|
||
|
const outputDir = args.d || args.device || Deno.env.get('SCANNER_OUTPUT_DIR');
|
||
|
const outputName = args._[0] || getRandomString();
|
||
|
const files = [];
|
||
|
let cancelled = false;
|
||
|
do {
|
||
|
const fileName = `${tempDir}/${getRandomString()}.jpg`;
|
||
|
const scanimageProcess = Deno.run({
|
||
|
cmd: [
|
||
|
"scanimage",
|
||
|
"-d",
|
||
|
device,
|
||
|
"--format=jpeg",
|
||
|
"--resolution",
|
||
|
"300",
|
||
|
"--progress",
|
||
|
"-o",
|
||
|
fileName,
|
||
|
],
|
||
|
stderr: 'piped'
|
||
|
});
|
||
|
const decoder = new TextDecoder();
|
||
|
const encoder = new TextEncoder();
|
||
|
let inScan = false;
|
||
|
for await (const chunk of iterateReader(scanimageProcess.stderr)){
|
||
|
const line = decoder.decode(chunk);
|
||
|
if (!inScan && line.startsWith('Progress: ')) {
|
||
|
inScan = true;
|
||
|
Deno.kill(scanimageProcess.pid, 'SIGSTOP');
|
||
|
const answer = prompt("Scan next page? [Y/n]");
|
||
|
if (answer && ![
|
||
|
'y',
|
||
|
'yes'
|
||
|
].includes(answer.toLowerCase())) {
|
||
|
Deno.kill(scanimageProcess.pid, 'SIGKILL');
|
||
|
cancelled = true;
|
||
|
break;
|
||
|
} else {
|
||
|
Deno.kill(scanimageProcess.pid, 'SIGCONT');
|
||
|
cancelled = false;
|
||
|
}
|
||
|
}
|
||
|
Deno.stdout.write(encoder.encode(`\r${line}`));
|
||
|
}
|
||
|
const status = await scanimageProcess.status();
|
||
|
scanimageProcess.stderr.close();
|
||
|
if (!cancelled && status.code !== 0) {
|
||
|
console.error('Scan seems to have failed with status ', status.code);
|
||
|
}
|
||
|
if (!cancelled) {
|
||
|
files.push(fileName);
|
||
|
}
|
||
|
}while (!cancelled)
|
||
|
const pdfFile = `${outputDir}/${outputName}.pdf`;
|
||
|
const img2pdfProcess = Deno.run({
|
||
|
cmd: [
|
||
|
"img2pdf",
|
||
|
"--output",
|
||
|
pdfFile,
|
||
|
...files,
|
||
|
]
|
||
|
});
|
||
|
const pdfStatus = await img2pdfProcess.status();
|
||
|
if (pdfStatus.code !== 0) {
|
||
|
console.error('Error writing PDF');
|
||
|
Deno.exit(2);
|
||
|
}
|
||
|
console.log(`PDF written to ${pdfFile}`);
|
||
|
})();
|
||
|
|