feat(scan): whiten receipt backgrounds and embed OCR in PDFs
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -38,7 +38,14 @@ const installCommand = computed(() => 'bash ' + filename.value)
|
||||
const tokenReady = computed(() => /^fedeo_agent_[a-f0-9]{64}$/.test(token.value.trim()))
|
||||
const online = computed(() => agentSetupReady(agent.value, checkingSince.value, now.value))
|
||||
const scannerOptions = computed(() => (agent.value?.scannerNames || []).map(value => ({ label: value, value })))
|
||||
const readyToSave = computed(() => online.value && agent.value?.capabilities?.scan && scannerOptions.value.some(item => item.value === selectedScanner.value))
|
||||
const setupScanDefaults = computed(() => profile.value === 'ix1600' ? { ...ix1600ScanDefaults } : {
|
||||
format: 'pdf', resolution: 300, mode: 'Color', paperSize: 'auto', postprocess: false,
|
||||
backgroundCleanup: true, ocr: true, ocrLanguage: 'deu+eng', ...agent.value?.scanDefaults,
|
||||
})
|
||||
const processingMissing = computed(() =>
|
||||
(setupScanDefaults.value.format === 'pdf' && setupScanDefaults.value.ocr && !agent.value?.capabilities?.ocr) ||
|
||||
(setupScanDefaults.value.backgroundCleanup && !agent.value?.capabilities?.backgroundCleanup))
|
||||
const readyToSave = computed(() => online.value && agent.value?.capabilities?.scan && !processingMissing.value && scannerOptions.value.some(item => item.value === selectedScanner.value))
|
||||
const endpointPreview = computed(() => {
|
||||
try { return agentBackendUrl(backendUrl.value) + '/instance-agent/heartbeat' } catch { return '' }
|
||||
})
|
||||
@@ -129,10 +136,7 @@ const finishSetup = async () => {
|
||||
if (!agent.value || !readyToSave.value || busy.value) return
|
||||
busy.value = true
|
||||
try {
|
||||
const scanDefaults = profile.value === 'ix1600' ? { ...ix1600ScanDefaults } : {
|
||||
format: 'pdf', resolution: 300, mode: 'Color', paperSize: 'auto', postprocess: false, ...agent.value.scanDefaults,
|
||||
}
|
||||
await admin.updateInstanceAgent(agent.value.id, { preferredScannerName: selectedScanner.value, scanDefaults })
|
||||
await admin.updateInstanceAgent(agent.value.id, { preferredScannerName: selectedScanner.value, scanDefaults: setupScanDefaults.value })
|
||||
saved.value = true
|
||||
token.value = ''
|
||||
stopPolling()
|
||||
@@ -212,7 +216,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<div class="space-y-4 rounded-lg bg-muted/40 p-5">
|
||||
<h3 class="font-semibold text-highlighted">Installation herunterladen</h3>
|
||||
<p class="text-sm text-muted">Die Datei enthält die Agent-Software und deine Konfiguration. Sie prüft Node.js ab Version 20, SANE und Python/Pillow und installiert fehlende Systempakete mit sudo.</p>
|
||||
<p class="text-sm text-muted">Die Datei enthält die Agent-Software und deine Konfiguration. Sie installiert bei Bedarf Node.js ab Version 20, SANE, Bildverarbeitung und OCR für Deutsch/Englisch mit sudo.</p>
|
||||
<p class="text-sm text-muted">Die Installation erfolgt in deinem Benutzerverzeichnis. Starte die Datei auf dem Rechner mit dem Scanner.</p>
|
||||
<UAlert v-if="configurationError" color="error" variant="soft" :title="configurationError" />
|
||||
<UButton icon="i-heroicons-arrow-down-tray" :disabled="!tokenReady || busy" @click="downloadInstaller">Linux-Installation herunterladen</UButton>
|
||||
@@ -253,6 +257,8 @@ onBeforeUnmount(() => {
|
||||
<UAlert v-if="!online" color="warning" variant="soft" title="Agent ist derzeit nicht verbunden" description="Agent starten und die Verbindung erneut prüfen." />
|
||||
<UAlert v-else-if="!agent?.capabilities?.scan || !scannerOptions.length" color="warning" variant="soft" title="Noch kein Scanner verfügbar"
|
||||
description="USB-Verbindung und Stromversorgung prüfen. scanimage -L muss den Scanner für denselben Linux-Benutzer anzeigen." />
|
||||
<UAlert v-else-if="processingMissing" color="warning" variant="soft" title="Scan-Verarbeitung noch nicht bereit"
|
||||
description="Für dieses Profil fehlen OCR oder Hintergrundbereinigung. Die aktuelle Linux-Installation aus Schritt 2 ausführen und anschließend den Agenten neu starten." />
|
||||
<UAlert v-if="connectionError" color="error" variant="soft" title="Status konnte nicht geprüft werden" :description="connectionError" />
|
||||
<UFormField label="Scanner" required>
|
||||
<USelectMenu v-model="selectedScanner" :items="scannerOptions" value-key="value" label-key="label" class="w-full" placeholder="Erkannten Scanner auswählen" />
|
||||
@@ -263,7 +269,7 @@ onBeforeUnmount(() => {
|
||||
{ label: 'Bisherige Einstellungen / allgemeiner Scanner', value: 'existing' }
|
||||
]" value-key="value" label-key="label" class="w-full" />
|
||||
</UFormField>
|
||||
<p class="text-sm text-muted">Das iX1600-Profil speichert den gesamten Stapel mit Vorder- und Rückseiten in einem PDF. Weitere Einstellungen kannst du anschließend in der Scannerverwaltung ändern.</p>
|
||||
<p class="text-sm text-muted">Das iX1600-Profil speichert Vorder- und Rückseiten als durchsuchbares PDF. Erkannter Scannerhintergrund außerhalb des Papiers wird weiß; das A4-Format bleibt erhalten.</p>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<UButton color="neutral" variant="outline" :loading="checking" @click="checkConnection">Scanner erneut suchen</UButton>
|
||||
<UButton :disabled="!readyToSave" :loading="busy" @click="finishSetup">Einrichtung abschließen</UButton>
|
||||
|
||||
@@ -34,6 +34,9 @@ const scanForm = reactive({
|
||||
source: "ADF Duplex",
|
||||
paperSize: "A4",
|
||||
postprocess: false,
|
||||
backgroundCleanup: true,
|
||||
ocr: true,
|
||||
ocrLanguage: "deu+eng",
|
||||
postprocessProfile: "document"
|
||||
})
|
||||
|
||||
@@ -49,7 +52,8 @@ watch(() => scanForm.format, (format) => {
|
||||
const applyIx1600Preset = () => {
|
||||
Object.assign(scanForm, {
|
||||
format: "pdf", resolution: 300, mode: "Color", source: "ADF Duplex",
|
||||
paperSize: "A4", postprocess: false, postprocessProfile: "document"
|
||||
paperSize: "A4", postprocess: false, postprocessProfile: "document",
|
||||
backgroundCleanup: true, ocr: true, ocrLanguage: "deu+eng"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,6 +87,9 @@ const applyAgentDefaults = (agent) => {
|
||||
scanForm.source = agent.scanDefaults?.source || "ADF Duplex"
|
||||
scanForm.paperSize = agent.scanDefaults?.paperSize || "A4"
|
||||
scanForm.postprocess = Boolean(agent.scanDefaults?.postprocess)
|
||||
scanForm.backgroundCleanup = agent.scanDefaults?.backgroundCleanup !== false
|
||||
scanForm.ocr = agent.scanDefaults?.ocr !== false
|
||||
scanForm.ocrLanguage = agent.scanDefaults?.ocrLanguage || "deu+eng"
|
||||
scanForm.postprocessProfile = agent.scanDefaults?.postprocessProfile || "document"
|
||||
if (isFeederScan.value) scanForm.format = "pdf"
|
||||
|
||||
@@ -147,6 +154,11 @@ const waitForJob = async (jobId) => {
|
||||
|
||||
const startScan = async () => {
|
||||
if (!selectedAgentId.value || scanInProgress.value) return
|
||||
if ((scanForm.format === "pdf" && scanForm.ocr && !selectedAgent.value?.capabilities?.ocr) ||
|
||||
(scanForm.backgroundCleanup && !selectedAgent.value?.capabilities?.backgroundCleanup)) {
|
||||
toast.add({ title: "Agent aktualisieren", description: "Für OCR und Hintergrundbereinigung benötigt der Scanner-Rechner die aktuelle Agent-Installation mit Tesseract und OpenCV.", color: "error" })
|
||||
return
|
||||
}
|
||||
|
||||
scanInProgress.value = true
|
||||
statusMessage.value = "Scan-Auftrag wird erstellt..."
|
||||
@@ -166,6 +178,9 @@ const startScan = async () => {
|
||||
source: scanForm.source || null,
|
||||
paperSize: scanForm.paperSize,
|
||||
postprocess: scanForm.postprocess,
|
||||
backgroundCleanup: scanForm.backgroundCleanup,
|
||||
ocr: scanForm.format === "pdf" && scanForm.ocr,
|
||||
ocrLanguage: scanForm.ocrLanguage,
|
||||
postprocessProfile: scanForm.postprocessProfile
|
||||
},
|
||||
target: {
|
||||
@@ -319,10 +334,23 @@ loadAgents()
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<UCheckbox v-model="scanForm.backgroundCleanup" label="Scannerhintergrund weiß machen" :disabled="scanInProgress" />
|
||||
<p class="text-xs text-muted">Erkannte Flächen außerhalb von Bon oder Dokument werden weiß. Das Seitenformat bleibt erhalten; bei unsicheren Rändern bleibt der Scan unverändert.</p>
|
||||
<UCheckbox v-model="scanForm.ocr" label="Durchsuchbares PDF (OCR)" :disabled="scanForm.format !== 'pdf' || scanInProgress" />
|
||||
<UFormField v-if="scanForm.ocr && scanForm.format === 'pdf'" label="OCR-Sprache">
|
||||
<USelectMenu v-model="scanForm.ocrLanguage" :items="[
|
||||
{ label: 'Deutsch und Englisch', value: 'deu+eng' },
|
||||
{ label: 'Deutsch', value: 'deu' }, { label: 'Englisch', value: 'eng' }
|
||||
]" value-key="value" label-key="label" :disabled="scanInProgress" />
|
||||
</UFormField>
|
||||
<p class="text-xs text-muted">Texterkennung erfolgt vor dem Upload auf dem Scanner-Rechner, auch für normale Dateien ohne Eingangsbeleg.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-[auto_minmax(0,1fr)]">
|
||||
<UCheckbox
|
||||
v-model="scanForm.postprocess"
|
||||
label="OpenCV-Korrektur"
|
||||
label="Zuschnitt und Ausrichtung zusätzlich korrigieren"
|
||||
:disabled="scanInProgress"
|
||||
/>
|
||||
<UFormField label="Profil">
|
||||
|
||||
@@ -144,6 +144,9 @@ export type InstanceAgent = {
|
||||
resolution?: number
|
||||
mode?: string
|
||||
source?: string | null
|
||||
backgroundCleanup?: boolean
|
||||
ocr?: boolean
|
||||
ocrLanguage?: string
|
||||
[key: string]: any
|
||||
}
|
||||
lastSeenAt?: string | null
|
||||
|
||||
@@ -31,6 +31,9 @@ const editState = reactive({
|
||||
source: "",
|
||||
paperSize: "A4",
|
||||
postprocess: false,
|
||||
backgroundCleanup: true,
|
||||
ocr: true,
|
||||
ocrLanguage: "deu+eng",
|
||||
postprocessProfile: "document",
|
||||
})
|
||||
|
||||
@@ -69,13 +72,17 @@ const applyAgentToForm = (agent: InstanceAgent | null) => {
|
||||
editState.source = agent.scanDefaults?.source || ""
|
||||
editState.paperSize = agent.scanDefaults?.paperSize || "A4"
|
||||
editState.postprocess = Boolean(agent.scanDefaults?.postprocess)
|
||||
editState.backgroundCleanup = agent.scanDefaults?.backgroundCleanup !== false
|
||||
editState.ocr = agent.scanDefaults?.ocr !== false
|
||||
editState.ocrLanguage = agent.scanDefaults?.ocrLanguage || "deu+eng"
|
||||
editState.postprocessProfile = agent.scanDefaults?.postprocessProfile || "document"
|
||||
}
|
||||
|
||||
const applyIx1600Preset = () => {
|
||||
Object.assign(editState, {
|
||||
format: "pdf", resolution: 300, mode: "Color", source: "ADF Duplex",
|
||||
paperSize: "A4", postprocess: false, postprocessProfile: "document"
|
||||
paperSize: "A4", postprocess: false, postprocessProfile: "document",
|
||||
backgroundCleanup: true, ocr: true, ocrLanguage: "deu+eng"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,6 +139,9 @@ const saveAgent = async () => {
|
||||
source: editState.source || null,
|
||||
paperSize: editState.paperSize,
|
||||
postprocess: editState.postprocess,
|
||||
backgroundCleanup: editState.backgroundCleanup,
|
||||
ocr: editState.format === "pdf" && editState.ocr,
|
||||
ocrLanguage: editState.ocrLanguage,
|
||||
postprocessProfile: editState.postprocessProfile,
|
||||
},
|
||||
})
|
||||
@@ -312,8 +322,21 @@ onMounted(async () => {
|
||||
]" value-key="value" label-key="label" />
|
||||
</UFormField>
|
||||
|
||||
<div class="space-y-3">
|
||||
<UCheckbox v-model="editState.backgroundCleanup" label="Scannerhintergrund weiß machen" />
|
||||
<p class="text-xs text-muted">Erkannte Flächen außerhalb von Bon oder Dokument werden weiß. Das Seitenformat bleibt erhalten; bei unsicheren Rändern bleibt der Scan unverändert.</p>
|
||||
<UCheckbox v-model="editState.ocr" label="Durchsuchbares PDF (OCR)" :disabled="editState.format !== 'pdf'" />
|
||||
<UFormField v-if="editState.ocr && editState.format === 'pdf'" label="OCR-Sprache">
|
||||
<USelectMenu v-model="editState.ocrLanguage" :items="[
|
||||
{ label: 'Deutsch und Englisch', value: 'deu+eng' },
|
||||
{ label: 'Deutsch', value: 'deu' }, { label: 'Englisch', value: 'eng' }
|
||||
]" value-key="value" label-key="label" />
|
||||
</UFormField>
|
||||
<p class="text-xs text-muted">Texterkennung erfolgt vor dem Upload auf dem Scanner-Rechner, auch für normale Dateien ohne Eingangsbeleg.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-[auto_minmax(0,1fr)]">
|
||||
<UCheckbox v-model="editState.postprocess" label="OpenCV-Nachbearbeitung" />
|
||||
<UCheckbox v-model="editState.postprocess" label="Zuschnitt und Ausrichtung zusätzlich korrigieren" />
|
||||
<UFormField label="Profil">
|
||||
<USelectMenu
|
||||
v-model="editState.postprocessProfile"
|
||||
|
||||
@@ -85,13 +85,15 @@ test('stale or inactive agents cannot finish setup; fresh reports save the chose
|
||||
await ui.run('finishSetup()')
|
||||
assert.equal(ui.run('online.value'), false)
|
||||
assert.equal(ui.requests.length, 0)
|
||||
ui.report({ ...baseAgent, lastSeenAt: new Date().toISOString(), scannerNames: [scanner], capabilities: { scan: true } })
|
||||
ui.report({ ...baseAgent, lastSeenAt: new Date().toISOString(), scannerNames: [scanner], capabilities: { scan: true, ocr: true, backgroundCleanup: true } })
|
||||
await ui.run('checkConnection()')
|
||||
ui.run('profile.value="ix1600"; step.value=3')
|
||||
await ui.run('finishSetup()')
|
||||
assert.equal(ui.run('saved.value'), true)
|
||||
assert.equal(ui.requests[0][2].preferredScannerName, scanner)
|
||||
assert.equal(ui.requests[0][2].scanDefaults.source, 'ADF Duplex')
|
||||
assert.equal(ui.requests[0][2].scanDefaults.ocr, true)
|
||||
assert.equal(ui.requests[0][2].scanDefaults.backgroundCleanup, true)
|
||||
assert.equal(ui.run('token.value'), '')
|
||||
assert.equal(ui.timers.size, 0)
|
||||
ui.run('openTestScan()')
|
||||
@@ -108,6 +110,16 @@ test('online agent without a detected scanner remains incomplete', async t => {
|
||||
assert.equal(ui.requests.length, 0)
|
||||
})
|
||||
|
||||
test('setup cannot finish with an agent missing the processing required by its profile', async t => {
|
||||
const ui = await setup(t, { ...baseAgent })
|
||||
ui.report({ ...baseAgent, lastSeenAt: new Date().toISOString(), scannerNames: [scanner], capabilities: { scan: true } })
|
||||
await ui.run('checkConnection()')
|
||||
ui.run('profile.value="ix1600"; step.value=3')
|
||||
await ui.run('finishSetup()')
|
||||
assert.equal(ui.run('processingMissing.value'), true)
|
||||
assert.equal(ui.requests.length, 0)
|
||||
})
|
||||
|
||||
test('connection errors stop automatic requests and are visible for retry', async t => {
|
||||
const ui = await setup(t, { ...baseAgent }, { getInstanceAgents: async () => { throw Error('offline') } })
|
||||
ui.run('step.value=2')
|
||||
|
||||
@@ -4,7 +4,7 @@ const fs = require('node:fs')
|
||||
const vm = require('node:vm')
|
||||
const vue = require('vue')
|
||||
|
||||
async function setup(getJob, scanData = {}, agents = [{ id: 'agent', active: true, capabilities: { scan: true }, scannerNames: ['fujitsu:ScanSnap iX1600:test'] }]) {
|
||||
async function setup(getJob, scanData = {}, agents = [{ id: 'agent', active: true, capabilities: { scan: true, ocr: true, backgroundCleanup: true }, scannerNames: ['fujitsu:ScanSnap iX1600:test'] }]) {
|
||||
const notices = []
|
||||
const requests = []
|
||||
let unmount
|
||||
@@ -38,6 +38,9 @@ test('duplex scanning continues past the former three-minute polling limit', asy
|
||||
const body = ui.requests.find(request => request.url === '/api/scan-jobs').options.body
|
||||
assert.equal(body.settings.source, 'ADF Duplex')
|
||||
assert.equal(body.settings.paperSize, 'A4')
|
||||
assert.equal(body.settings.ocr, true)
|
||||
assert.equal(body.settings.ocrLanguage, 'deu+eng')
|
||||
assert.equal(body.settings.backgroundCleanup, true)
|
||||
assert.equal(body.target.folder, 'folder-1')
|
||||
})
|
||||
|
||||
@@ -72,7 +75,7 @@ test('iX1600 preset resets image settings and normalizes the output filename to
|
||||
})
|
||||
|
||||
test('setup test scan uses the requested agent even when another agent is first', async () => {
|
||||
const agents = ['first', 'configured'].map(id => ({ id, active: true, capabilities: { scan: true }, scannerNames: ['scanner-' + id] }))
|
||||
const agents = ['first', 'configured'].map(id => ({ id, active: true, capabilities: { scan: true, ocr: true, backgroundCleanup: true }, scannerNames: ['scanner-' + id] }))
|
||||
const ui = await setup(() => ({ job: { status: 'completed' } }), { agentId: 'configured' }, agents)
|
||||
await ui.run('startScan()')
|
||||
assert.equal(ui.requests.find(request => request.url === '/api/scan-jobs').options.body.agentId, 'configured')
|
||||
@@ -83,3 +86,10 @@ test('an unavailable setup agent never silently scans with another device', asyn
|
||||
await ui.run('startScan()')
|
||||
assert.equal(ui.requests.filter(request => request.url === '/api/scan-jobs').length, 0)
|
||||
})
|
||||
|
||||
test('an old agent cannot silently produce an image-only PDF when OCR was requested', async () => {
|
||||
const ui = await setup(() => ({}), {}, [{ id: 'old', active: true, capabilities: { scan: true }, scannerNames: ['scanner'] }])
|
||||
await ui.run('startScan()')
|
||||
assert.equal(ui.requests.filter(request => request.url === '/api/scan-jobs').length, 0)
|
||||
assert.equal(ui.notices[0].title, 'Agent aktualisieren')
|
||||
})
|
||||
|
||||
@@ -26,6 +26,7 @@ export function renderAgentInstaller(bundle, options) {
|
||||
'FEDEO_POLL_SECONDS=5', 'FEDEO_SCAN_FORMAT=pdf', 'FEDEO_SCAN_RESOLUTION=300',
|
||||
'FEDEO_SCAN_MODE=Color', 'FEDEO_SCAN_TIMEOUT_SECONDS=900',
|
||||
'FEDEO_SCAN_POSTPROCESS=false', 'FEDEO_SCAN_POSTPROCESS_PROFILE=document', '',
|
||||
'FEDEO_SCAN_BACKGROUND_CLEANUP=true', 'FEDEO_SCAN_OCR=true', 'FEDEO_SCAN_OCR_LANGUAGE=deu+eng', '',
|
||||
].join('\n')
|
||||
const replacements = {
|
||||
AGENT_ID: options.agentId, AUTOSTART: String(Boolean(options.autostart)),
|
||||
@@ -40,6 +41,7 @@ export function renderAgentInstaller(bundle, options) {
|
||||
export const ix1600ScanDefaults = {
|
||||
format: 'pdf', resolution: 300, mode: 'Color', source: 'ADF Duplex',
|
||||
paperSize: 'A4', postprocess: false, postprocessProfile: 'document',
|
||||
backgroundCleanup: true, ocr: true, ocrLanguage: 'deu+eng',
|
||||
}
|
||||
|
||||
export function agentSetupReady(agent, since = 0, now = Date.now()) {
|
||||
|
||||
Reference in New Issue
Block a user