From f085703c34ecbe3f3356fde01b93ffe218982e93 Mon Sep 17 00:00:00 2001 From: sam123ben Date: Mon, 12 Oct 2026 22:22:54 +1100 Subject: [PATCH] fix(sandbox): accept PDF and image uploads in file picker --- Cargo.lock | 1 + Dockerfile.sandbox | 3 +- crates/ocr-sandbox/Cargo.toml | 1 + crates/ocr-sandbox/src/api.rs | 102 ++++++++++++++++-- crates/ocr-sandbox/src/error.rs | 16 ++- crates/ocr-sandbox/src/web.rs | 9 ++ .../tests/fixtures/ocr-upload-test.jpeg | Bin 0 -> 40375 bytes .../tests/fixtures/ocr-upload-test.pdf | Bin 0 -> 25528 bytes .../tests/fixtures/ocr-upload-test.png | Bin 0 -> 22085 bytes .../ocr-sandbox/tests/fixtures/two-page.pdf | Bin 0 -> 50626 bytes crates/ocr-sandbox/web/app.mjs | 38 ++++--- crates/ocr-sandbox/web/app.test.mjs | 4 +- crates/ocr-sandbox/web/document.mjs | 11 ++ crates/ocr-sandbox/web/document.test.mjs | 22 ++++ crates/ocr-sandbox/web/index.html | 16 +-- crates/ocr-sandbox/web/upload.test.mjs | 9 ++ docs/OCR-SANDBOX.md | 2 +- docs/adr/0015-public-ocr-sandbox.md | 4 +- 18 files changed, 201 insertions(+), 37 deletions(-) create mode 100644 crates/ocr-sandbox/tests/fixtures/ocr-upload-test.jpeg create mode 100644 crates/ocr-sandbox/tests/fixtures/ocr-upload-test.pdf create mode 100644 crates/ocr-sandbox/tests/fixtures/ocr-upload-test.png create mode 100644 crates/ocr-sandbox/tests/fixtures/two-page.pdf create mode 100644 crates/ocr-sandbox/web/document.mjs create mode 100644 crates/ocr-sandbox/web/document.test.mjs create mode 100644 crates/ocr-sandbox/web/upload.test.mjs diff --git a/Cargo.lock b/Cargo.lock index 9f10b97..96767b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2522,6 +2522,7 @@ dependencies = [ "http-body-util", "image", "object_store", + "ocr-service", "reqwest", "serde", "serde_json", diff --git a/Dockerfile.sandbox b/Dockerfile.sandbox index fbfdeff..2bc68ac 100644 --- a/Dockerfile.sandbox +++ b/Dockerfile.sandbox @@ -7,11 +7,12 @@ RUN apt-get update && apt-get install --no-install-recommends -y build-essential COPY Cargo.toml Cargo.lock ./ COPY crates ./crates COPY benchmarks ./benchmarks -RUN cargo build --locked --release -p ocr-sandbox +RUN cargo build --locked --release -p ocr-sandbox -p ocr-parser-sandbox FROM ${TESSERIX_RUNTIME} WORKDIR /app COPY LICENSE /licenses/document-intelligence.LICENSE COPY --from=build --chown=10001:10001 /src/target/release/ocr-sandbox /app/ocr-sandbox +COPY --from=build --chown=10001:10001 /src/target/release/ocr-parser-sandbox /app/ocr-parser-sandbox USER 10001:10001 LABEL org.opencontainers.image.source="https://github.com/tesserix/document-intelligence" \ org.opencontainers.image.licenses="Apache-2.0" diff --git a/crates/ocr-sandbox/Cargo.toml b/crates/ocr-sandbox/Cargo.toml index f15b78b..04c0aa9 100644 --- a/crates/ocr-sandbox/Cargo.toml +++ b/crates/ocr-sandbox/Cargo.toml @@ -14,6 +14,7 @@ futures-util.workspace = true hmac.workspace = true image.workspace = true object_store.workspace = true +ocr-service = { path = "../ocr-service" } reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/ocr-sandbox/src/api.rs b/crates/ocr-sandbox/src/api.rs index 795dc3e..da37044 100644 --- a/crates/ocr-sandbox/src/api.rs +++ b/crates/ocr-sandbox/src/api.rs @@ -25,6 +25,8 @@ pub struct App { pub(crate) storage: Storage, pub(crate) upstream: Upstream, permits: Semaphore, + pdf_permits: Semaphore, + parser: ocr_service::ParserProcess, } impl App { @@ -35,6 +37,12 @@ impl App { upstream: Upstream::new(&config)?, config, permits: Semaphore::new(8), + pdf_permits: Semaphore::new(1), + parser: ocr_service::ParserProcess::new( + "/app/ocr-parser-sandbox".into(), + std::time::Duration::from_secs(5), + ) + .map_err(|_| Error::Configuration)?, })) } @@ -163,8 +171,10 @@ struct UploadInput { fn validate_upload(body: &Value) -> Result { let input: UploadInput = serde_json::from_value(body.clone()).map_err(|_| Error::Invalid)?; let digest = input.sha256.strip_prefix("sha256:").ok_or(Error::Invalid)?; - if !matches!(input.content_type.as_str(), "image/jpeg" | "image/png") - || !(1..=MAX_BYTES).contains(&input.content_length) + if !matches!( + input.content_type.as_str(), + "application/pdf" | "image/jpeg" | "image/png" + ) || !(1..=MAX_BYTES).contains(&input.content_length) || digest.len() != 64 || !digest .bytes() @@ -255,6 +265,33 @@ fn validate_image(body: &[u8], input: &UploadInput) -> Result<(), Error> { Ok(()) } +async fn validate_pdf( + body: &[u8], + input: &UploadInput, + parser: &ocr_service::ParserProcess, +) -> Result<(), Error> { + if body.len() != input.content_length + || body.len() > MAX_BYTES + || format!("sha256:{}", hash(body)) != input.sha256 + || !body.starts_with(b"%PDF-") + { + return Err(Error::Invalid); + } + let report = parser + .inspect(body, "application/pdf") + .await + .map_err(|error| match error { + ocr_service::ParserProcessError::PasswordRequired => Error::PasswordProtected, + ocr_service::ParserProcessError::InvalidDocument + | ocr_service::ParserProcessError::LimitsExceeded => Error::InvalidDocument, + _ => Error::Unavailable, + })?; + if report.page_count != 1 || report.maximum_page_pixels > MAX_PIXELS as i64 { + return Err(Error::InvalidDocument); + } + Ok(()) +} + async fn upload_content( State(app): State>, Path(id): Path, @@ -265,9 +302,14 @@ async fn upload_content( owned(&session, &id, false)?; let input = validate_upload(session.upload_request.as_ref().ok_or(Error::Conflict)?)?; let bytes = body.clone(); - tokio::task::spawn_blocking(move || validate_image(&bytes, &input)) - .await - .map_err(|_| Error::Unavailable)??; + if input.content_type == "application/pdf" { + let _permit = app.pdf_permits.try_acquire().map_err(|_| Error::Limited)?; + validate_pdf(&bytes, &input, &app.parser).await?; + } else { + tokio::task::spawn_blocking(move || validate_image(&bytes, &input)) + .await + .map_err(|_| Error::Unavailable)??; + } if session.content_uploaded { return Ok(StatusCode::NO_CONTENT); } @@ -498,6 +540,52 @@ async fn job_result( #[cfg(test)] mod tests { use serde_json::json; + #[tokio::test] + async fn sandbox_pdf_validation_checks_bytes_and_one_page_budget() { + let path = std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .join("ocr-parser-sandbox"); + let parser = + ocr_service::ParserProcess::new(path, std::time::Duration::from_secs(5)).unwrap(); + for (bytes, accepted) in [ + ( + include_bytes!("../tests/fixtures/ocr-upload-test.pdf").as_slice(), + true, + ), + ( + include_bytes!("../tests/fixtures/two-page.pdf").as_slice(), + false, + ), + ( + include_bytes!("../tests/fixtures/ocr-upload-test.png").as_slice(), + false, + ), + (b"%PDF-1.7\nnot a document".as_slice(), false), + ] { + let input = super::UploadInput { + content_type: "application/pdf".into(), + content_length: bytes.len(), + sha256: format!("sha256:{}", crate::store::hash(bytes)), + }; + assert_eq!( + super::validate_pdf(bytes, &input, &parser).await.is_ok(), + accepted + ); + } + } + #[test] + fn sandbox_accepts_pdf_upload_reservations() { + let digest = format!("sha256:{}", "a".repeat(64)); + assert!(super::validate_upload( + &json!({"content_type":"application/pdf","content_length":100,"sha256":digest}) + ) + .is_ok()); + } + #[test] fn sandbox_rejects_unknown_or_non_boolean_output_options() { for output in [ @@ -515,14 +603,14 @@ mod tests { } #[test] - fn sandbox_rejects_documents_outside_its_image_budget() { + fn sandbox_rejects_documents_outside_its_upload_budget() { let digest = format!("sha256:{}", "a".repeat(64)); assert!(super::validate_upload( &json!({"content_type":"image/png","content_length":100,"sha256":digest}) ) .is_ok()); for body in [ - json!({"content_type":"application/pdf","content_length":100,"sha256":digest}), + json!({"content_type":"image/tiff","content_length":100,"sha256":digest}), json!({"content_type":"image/png","content_length":4194305,"sha256":digest}), json!({"content_type":"image/png","content_length":100,"sha256":digest,"tenant_id":"ten_OTHER"}), ] { diff --git a/crates/ocr-sandbox/src/error.rs b/crates/ocr-sandbox/src/error.rs index e5a74b3..fe0ca19 100644 --- a/crates/ocr-sandbox/src/error.rs +++ b/crates/ocr-sandbox/src/error.rs @@ -13,6 +13,10 @@ pub enum Error { Unavailable, #[error("invalid request")] Invalid, + #[error("invalid document or document budget exceeded")] + InvalidDocument, + #[error("password-protected document")] + PasswordProtected, #[error("session expired or invalid")] Unauthorized, #[error("object not found")] @@ -34,6 +38,16 @@ impl From for Error { impl IntoResponse for Error { fn into_response(self) -> Response { let (status, code, message) = match self { + Self::InvalidDocument => ( + StatusCode::BAD_REQUEST, + "InvalidDocument", + "Choose a valid PDF with one page and at most 12 megapixels.", + ), + Self::PasswordProtected => ( + StatusCode::BAD_REQUEST, + "PasswordProtected", + "Password-protected PDFs cannot be processed in this sandbox.", + ), Self::Invalid => ( StatusCode::BAD_REQUEST, "InvalidRequest", @@ -57,7 +71,7 @@ impl IntoResponse for Error { Self::Conflict => ( StatusCode::CONFLICT, "SessionConflict", - "This session already has an image or a request in progress.", + "This session already has a document or a request in progress.", ), Self::Upstream(status) if status.is_client_error() => ( status, diff --git a/crates/ocr-sandbox/src/web.rs b/crates/ocr-sandbox/src/web.rs index 5d3c274..768c5b6 100644 --- a/crates/ocr-sandbox/src/web.rs +++ b/crates/ocr-sandbox/src/web.rs @@ -34,6 +34,15 @@ pub fn router() -> Router> { ) }), ) + .route( + "/document.mjs", + get(|| async { + ( + [("content-type", "text/javascript; charset=utf-8")], + include_str!("../web/document.mjs"), + ) + }), + ) .route( "/score.mjs", get(|| async { diff --git a/crates/ocr-sandbox/tests/fixtures/ocr-upload-test.jpeg b/crates/ocr-sandbox/tests/fixtures/ocr-upload-test.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..739713bd71e30b23b1fa3dce7d62ecf539cfc113 GIT binary patch literal 40375 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 literal 0 HcmV?d00001 diff --git a/crates/ocr-sandbox/tests/fixtures/ocr-upload-test.pdf b/crates/ocr-sandbox/tests/fixtures/ocr-upload-test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..624cea4993b002b6db5aa0da365ffb139bdd11de GIT binary patch literal 25528 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 literal 0 HcmV?d00001 diff --git a/crates/ocr-sandbox/tests/fixtures/ocr-upload-test.png b/crates/ocr-sandbox/tests/fixtures/ocr-upload-test.png new file mode 100644 index 0000000000000000000000000000000000000000..b2e6f9914223ac04a548089b6452ae3f65c35b23 GIT binary patch literal 22085 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≶@P~r0@UXoF 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 Hbe{bW<_6RX literal 0 HcmV?d00001 diff --git a/crates/ocr-sandbox/tests/fixtures/two-page.pdf b/crates/ocr-sandbox/tests/fixtures/two-page.pdf new file mode 100644 index 0000000000000000000000000000000000000000..afab2f07e33abfb9c474db525f4067521f7bdd1e GIT binary patch literal 50626 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 iB-VGQQvfWthsZk)}^a&)Sme%)nmVX6|3n@_m literal 0 HcmV?d00001 diff --git a/crates/ocr-sandbox/web/app.mjs b/crates/ocr-sandbox/web/app.mjs index e01ccdd..f03b149 100644 --- a/crates/ocr-sandbox/web/app.mjs +++ b/crates/ocr-sandbox/web/app.mjs @@ -1,5 +1,6 @@ import {isTerminalJob,isFailedJob} from './status.mjs'; import {score} from './score.mjs'; +import {prepareFile} from './document.mjs'; const $=id=>document.getElementById(id); let selected=null,reference=null,previewUrl=null,lastResult=null,busy=false,runNumber=0; const pause=ms=>new Promise(resolve=>setTimeout(resolve,ms)); @@ -29,19 +30,24 @@ async function poll(path,token,states){ throw new Error('This document is taking longer than expected. The sandbox will finish or cancel the job and clean up its images automatically.'); } -function choose(file,sample=null){ +async function choose(file,sample=null){ $('error').hidden=true; - if(!['image/png','image/jpeg'].includes(file.type)||file.size>4194304||file.size===0){selected=null;$('run').disabled=true;error('Choose a PNG or JPEG image up to 4 MiB.');return;} - selected=file;reference=sample; + selected=null;reference=null;setBusy(true); if(previewUrl)URL.revokeObjectURL(previewUrl); - previewUrl=URL.createObjectURL(file);$('preview').src=previewUrl;$('preview-wrap').hidden=false; - $('file-title').textContent='Change selected image';$('file-hint').textContent='Choose another file or drop it here'; + previewUrl=null;$('preview-wrap').hidden=true; + try{file=await prepareFile(file);}catch(failure){error(failure.message);setBusy(false);return;} + selected=file;reference=sample; + const pdf=file.type==='application/pdf'; + $('preview').hidden=pdf;$('pdf-preview').hidden=!pdf; + if(!pdf){previewUrl=URL.createObjectURL(file);$('preview').src=previewUrl;} + $('preview-wrap').hidden=false; + $('file-title').textContent='Change selected document';$('file-hint').textContent='Choose another file or drop it here'; $('file-meta').textContent=`${file.name} · ${(file.size/1024).toFixed(0)} KiB${sample?' · benchmark sample':''}`; - $('nutrition').checked=Boolean(sample?.extraction);$('run').disabled=busy; + $('nutrition').checked=Boolean(sample?.extraction);setBusy(false); } $('dropzone').addEventListener('click',()=>$('file').click()); -$('file').addEventListener('change',event=>{if(event.target.files[0])choose(event.target.files[0]);}); +$('file').addEventListener('change',async event=>{if(event.target.files[0])await choose(event.target.files[0]);}); for(const name of ['dragover','dragleave','drop'])$('dropzone').addEventListener(name,event=>{event.preventDefault();if(busy)return;$('dropzone').classList.toggle('drag',name==='dragover');if(name==='drop'&&event.dataTransfer.files[0])choose(event.dataTransfer.files[0]);}); for(const button of document.querySelectorAll('[data-sample]'))button.addEventListener('click',async()=>{ setBusy(true); @@ -49,7 +55,7 @@ for(const button of document.querySelectorAll('[data-sample]'))button.addEventLi const manifest=await fetch('/samples/manifest.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw new Error('Samples are unavailable.');return r.json();}); const sample=manifest.cases.find(item=>item.id===button.dataset.sample); const response=await fetch(`/samples/${sample.id}.png`);if(!response.ok)throw new Error('The sample image is unavailable.'); - choose(new File([await response.blob()],`${sample.id}.png`,{type:'image/png'}),sample); + await choose(new File([await response.blob()],`${sample.id}.png`,{type:'image/png'}),sample); }catch(failure){error(failure.message);}finally{setBusy(false);} }); @@ -59,7 +65,7 @@ function render(result,status,elapsed,benchmark,deleted){ if(benchmark?.kind==='fields'){$('benchmark').textContent=`${benchmark.correct} / ${benchmark.total}`;$('benchmark-note').textContent='Expected reference fields matched. A sample result is not a guarantee for other images.';} else if(benchmark?.kind==='text'){$('benchmark').textContent=`${(benchmark.characterErrorRate*100).toFixed(1)}% CER`;$('benchmark-note').textContent='Character error rate against the sample reference after whitespace normalization. Lower is better.';} else if(benchmark?.kind==='blank'){$('benchmark').textContent=benchmark.passed?'No invented text':'Unexpected text';$('benchmark-note').textContent='The reference image contains no readable text.';} - else{$('benchmark').textContent='Not scored';$('benchmark-note').textContent='Your image has no known reference. We do not infer an accuracy percentage.';} + else{$('benchmark').textContent='Not scored';$('benchmark-note').textContent='Your document has no known reference. We do not infer an accuracy percentage.';} cleanupStatus(deleted); $('text').textContent=result.text||'No readable text detected.';$('json').textContent=JSON.stringify(result,null,2); const warnings=[...(result.warnings??[]),...(result.validation_failures??[])]; @@ -72,7 +78,7 @@ function render(result,status,elapsed,benchmark,deleted){ } $('result-title').focus(); } -function cleanupStatus(deleted){$('cleanup').textContent=deleted?'✓ Server image copies deleted and absence verified.':'Image deletion pending. Server-side cleanup will retry automatically.';$('cleanup').classList.toggle('pending',!deleted);} +function cleanupStatus(deleted){$('cleanup').textContent=deleted?'✓ Server document copies deleted and absence verified.':'Document deletion pending. Server-side cleanup will retry automatically.';$('cleanup').classList.toggle('pending',!deleted);} $('run').addEventListener('click',async()=>{ if(busy||!selected)return; @@ -85,22 +91,22 @@ $('run').addEventListener('click',async()=>{ const digest=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',await file.arrayBuffer())),n=>n.toString(16).padStart(2,'0')).join(''); const upload=(await request('/v1/ocr/uploads',{token,method:'POST',body:{content_type:file.type,content_length:file.size,sha256:`sha256:${digest}`}})).value; const uploadUrl=new URL(upload.upload_url);if(uploadUrl.origin!==location.origin)throw new Error('The sandbox returned an invalid upload destination.'); - stage('upload','Uploading your image…');await request(uploadUrl.pathname,{token,method:'PUT',body:file,headers:upload.required_headers}); + stage('upload','Uploading your document…');await request(uploadUrl.pathname,{token,method:'PUT',body:file,headers:upload.required_headers}); await request(`/v1/ocr/uploads/${upload.upload_id}/complete`,{token,method:'POST',body:{}}); - stage('inspect','Checking file safety and image structure…'); + stage('inspect','Checking file safety and document structure…'); const inspection=await poll(`/v1/ocr/uploads/${upload.upload_id}`,token,status=>['accepted','rejected','expired'].includes(status)); - if(inspection.status!=='accepted')throw new Error('The image did not pass inspection. Please try another image; cleanup will run automatically.'); + if(inspection.status!=='accepted')throw new Error('The document did not pass inspection. Please try another document; cleanup will run automatically.'); const payload={source:{upload_id:upload.upload_id},document_type:'general',output:{text:true,markdown:false,layout:true,evidence:true},language_hints:['en'],processing_class:'interactive'}; if($('nutrition').checked)payload.extraction={schema_id:'kora.nutrition_label',schema_version:'1'}; jobId=(await request('/v1/ocr/jobs',{token,method:'POST',body:payload})).value.job_id; - stage('ocr','Reading the image with the OCR service…');const state=await poll(`/v1/ocr/jobs/${jobId}`,token,isTerminalJob); - if(isFailedJob(state.status))throw new Error('OCR could not complete this image. Cleanup continues automatically.'); + stage('ocr','Reading the document with the OCR service…');const state=await poll(`/v1/ocr/jobs/${jobId}`,token,isTerminalJob); + if(isFailedJob(state.status))throw new Error('OCR could not complete this document. Cleanup continues automatically.'); const {value:result,response}=await request(`/v1/ocr/jobs/${jobId}/result`,{token}); const deleted=response.headers.get('x-sandbox-images-deleted')==='true'; render(result,state.status,performance.now()-start,score(sample?.sha256===digest?sample:null,result),deleted); stage('done','OCR finished. Inspect the output below.'); if(!deleted)void waitForCleanup(token,number); - }catch(failure){error(failure.message);$('result-state').textContent='Could not complete';$('status').textContent='You can try another image. Cleanup continues independently.';if(token&&jobId)try{await request(`/v1/ocr/jobs/${jobId}/cancel`,{token,method:'POST',body:{}});}catch{/* The durable cleanup worker retries cancellation. */}} + }catch(failure){error(failure.message);$('result-state').textContent='Could not complete';$('status').textContent='You can try another document. Cleanup continues independently.';if(token&&jobId)try{await request(`/v1/ocr/jobs/${jobId}/cancel`,{token,method:'POST',body:{}});}catch{/* The durable cleanup worker retries cancellation. */}} finally{setBusy(false);} }); diff --git a/crates/ocr-sandbox/web/app.test.mjs b/crates/ocr-sandbox/web/app.test.mjs index 648ecd8..5967049 100644 --- a/crates/ocr-sandbox/web/app.test.mjs +++ b/crates/ocr-sandbox/web/app.test.mjs @@ -31,8 +31,8 @@ test('browser stops polling rejected jobs and restores upload controls', async ( }; try { await import('./app.mjs'); - const file = new File(['synthetic image'], 'fixture.png', {type: 'image/png'}); - element('file').listeners.change({target: {files: [file]}}); + const file = new File([new Uint8Array([137,80,78,71,13,10,26,10])], 'fixture.png', {type: 'image/png'}); + await element('file').listeners.change({target: {files: [file]}}); await element('run').listeners.click(); assert.equal(polls, 1); assert.equal(results, 0); diff --git a/crates/ocr-sandbox/web/document.mjs b/crates/ocr-sandbox/web/document.mjs new file mode 100644 index 0000000..3430885 --- /dev/null +++ b/crates/ocr-sandbox/web/document.mjs @@ -0,0 +1,11 @@ +export async function prepareFile(file) { + const reject = () => { throw new Error('Choose a PDF, PNG or JPEG file up to 4 MiB.'); }; + if (file.size === 0 || file.size > 4194304) reject(); + const header = new Uint8Array(await file.slice(0, 12).arrayBuffer()); + const starts = bytes => bytes.every((value, index) => header[index] === value); + const type = starts([137,80,78,71,13,10,26,10]) ? 'image/png' + : starts([255,216,255]) ? 'image/jpeg' + : starts([37,80,68,70,45]) ? 'application/pdf' : null; + if (!type || (file.type && !['application/octet-stream',type,...(type === 'image/jpeg' ? ['image/jpg'] : [])].includes(file.type))) reject(); + return new File([file], file.name, {type, lastModified:file.lastModified}); +} diff --git a/crates/ocr-sandbox/web/document.test.mjs b/crates/ocr-sandbox/web/document.test.mjs new file mode 100644 index 0000000..65532fd --- /dev/null +++ b/crates/ocr-sandbox/web/document.test.mjs @@ -0,0 +1,22 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {prepareFile} from './document.mjs'; + +const png = new Uint8Array([137,80,78,71,13,10,26,10,0]); +const jpeg = new Uint8Array([255,216,255,224,0]); +const pdf = '%PDF-1.7\nfixture'; + +for (const [name,bytes,type] of [['sample.png',png,'image/png'],['sample.jpg',jpeg,'image/jpeg'],['sample.jpeg',jpeg,'image/jpeg'],['sample.pdf',pdf,'application/pdf']]) { + test(`selects ${name} when the operating system supplies no MIME type`, async () => { + const file = await prepareFile(new File([bytes],name)); + assert.equal(file.type,type); + assert.equal(file.name,name); + assert.equal(file.size,new Blob([bytes]).size); + }); +} + +test('rejects spoofed MIME types, unsupported content, empty and oversized files', async () => { + for (const file of [new File([pdf],'fake.png',{type:'image/png'}),new File([''],'fake.png'),new File([],'empty.pdf'),new File([new Uint8Array(4194305)],'large.png')]) { + await assert.rejects(prepareFile(file),/PDF, PNG or JPEG/); + } +}); diff --git a/crates/ocr-sandbox/web/index.html b/crates/ocr-sandbox/web/index.html index defc301..ac0c25e 100644 --- a/crates/ocr-sandbox/web/index.html +++ b/crates/ocr-sandbox/web/index.html @@ -2,25 +2,25 @@ - + OCR Sandbox · Tesserix
-

REAL OCR. SMALL, SHARED SANDBOX.

From image
to something readable.

See what our OCR service can read. Upload an image or try a known sample, then inspect the text, fields and benchmark results.

OCR onlyNo chat or generative AIAutomatic image cleanup
+

REAL OCR. SMALL, SHARED SANDBOX.

From document
to something readable.

See what our OCR service can read. Upload a PDF, JPEG or PNG file, or try a known sample, then inspect the text, fields and benchmark results.

OCR onlyNo chat or generative AIAutomatic image cleanup
-
01

Choose an image

PNG / JPG
-
Image controls - - +
01

Choose a document

PDF / PNG / JPEG
+
Document controls + +

Or start with a benchmark

Known reference
-

Please use non-sensitive test images. Server copies are deleted after OCR finishes; failed cleanup is retried. Your local file and browser preview remain yours. How it works ↓

+

Please use non-sensitive test documents. Server document copies are deleted after OCR finishes; failed cleanup is retried. Your local file and browser preview remain yours. How it works ↓

02

Inspect the result

Ready when you are
@@ -33,6 +33,6 @@
-

BUILT FOR A QUICK CHECK

Your image is a test,
not a training dataset.

This sandbox calls the same OCR upload, inspection and processing APIs used by the service. It adds no chat model, JEV or generative extraction. OCR processing itself uses Google Document AI.

Images are removed from dedicated sandbox storage once processing reaches a terminal state. Results and page artifacts expire with the session. Cleanup keeps running if you close the page; outages can delay deletion, and the UI reports it as pending. Minimal job and quota metadata is retained separately. Provider processing is subject to its own data policy.

TRY THE API, TOO

Same journey.
Your own client.

Start a short-lived sandbox session, then reserve an upload, send the image, create a job and fetch its result. The session token stays in your browser memory and expires after 15 minutes.

View the API walkthrough ↗

One image per session. Shared daily upload budget. Session and request limits apply. This is a capability demo, not a production endpoint.

+

BUILT FOR A QUICK CHECK

Your document is a test,
not a training dataset.

This sandbox calls the same OCR upload, inspection and processing APIs used by the service. It adds no chat model, JEV or generative extraction. OCR processing itself uses Google Document AI.

Documents are removed from dedicated sandbox storage once processing reaches a terminal state. Results and page artifacts expire with the session. Cleanup keeps running if you close the page; outages can delay deletion, and the UI reports it as pending. Minimal job and quota metadata is retained separately. Provider processing is subject to its own data policy.

TRY THE API, TOO

Same journey.
Your own client.

Start a short-lived sandbox session, then reserve an upload, send the document, create a job and fetch its result. The session token stays in your browser memory and expires after 15 minutes.

View the API walkthrough ↗

One document per session. Shared daily upload budget. Session and request limits apply. This is a capability demo, not a production endpoint.

Tesserix · Document intelligenceOpen source. Inspectable results.
diff --git a/crates/ocr-sandbox/web/upload.test.mjs b/crates/ocr-sandbox/web/upload.test.mjs new file mode 100644 index 0000000..8a2e6c8 --- /dev/null +++ b/crates/ocr-sandbox/web/upload.test.mjs @@ -0,0 +1,9 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFile} from 'node:fs/promises'; + +test('native file picker includes PDF, PNG and both JPEG extensions', async () => { + const html = await readFile(new URL('./index.html', import.meta.url), 'utf8'); + const accept = html.match(/id="file"[^>]*accept="([^"]+)"/)[1].split(','); + for (const extension of ['.pdf', '.png', '.jpg', '.jpeg']) assert.ok(accept.includes(extension), extension); +}); diff --git a/docs/OCR-SANDBOX.md b/docs/OCR-SANDBOX.md index 5dcdcbf..1facc87 100644 --- a/docs/OCR-SANDBOX.md +++ b/docs/OCR-SANDBOX.md @@ -12,7 +12,7 @@ the examples below. ## What you can test -- Upload one JPEG or PNG (at most 4 MiB and 12 megapixels). +- Upload one PDF, JPEG or PNG document (at most 4 MiB, one page and 12 megapixels). - Read extracted text, warnings, provider confidence, and optional nutrition fields. - Download the result JSON. - Compare the bundled nutrition label, receipt and blank-image samples against diff --git a/docs/adr/0015-public-ocr-sandbox.md b/docs/adr/0015-public-ocr-sandbox.md index 49b43cd..1cc6d1d 100644 --- a/docs/adr/0015-public-ocr-sandbox.md +++ b/docs/adr/0015-public-ocr-sandbox.md @@ -7,7 +7,7 @@ Serve a small static UI and a server-side API adapter from one GKE service at perform durable cleanup. The adapter calls the existing OCR upload/job APIs; it never calls a chat model, JEV or a generative extraction service. -Initial limits: JPEG/PNG only, one image per session, 4 MiB and 12 megapixels, +Initial limits: PDF/JPEG/PNG, one document and one page per session, 4 MiB and 12 megapixels, ten sessions per network per hour, 100 uploads per UTC day across the deployment, 60 API requests per session per minute, and a 15-minute session lifetime. At 100 images/day this is at most 400 MiB/day of original uploads before deletion; @@ -34,6 +34,8 @@ The assets are visitor documents, API signing credentials and the OCR budget. Threats include anonymous automation, cross-session access and hostile files. The browser-to-adapter, adapter-to-OCR and adapter-to-storage boundaries validate input separately. The private OCR service retains its malware/parser checks. +PDF intake reuses the existing parser in a separate process with a five-second deadline; its environment is cleared and only one PDF inspection runs at a time. Require exactly one page and at most 12 megapixels before forwarding content, preserving the 100-page/day budget. Malformed, encrypted and over-budget PDFs fail closed; parser unavailability returns 503. The one-page cap avoids an increase in provider spend. No service, database, migration or retention policy is added. Rollback restores the earlier adapter image through GitOps; cleanup stays running. + No session token, signed URL, image or OCR text is logged. The frontend renders text as text, never HTML. No localStorage, analytics or third-party UI scripts.
tesserix / OCR sandboxSource & docs ↗