From 5638a20d8da5b2e7bd72b4889d0ec4945dc7d823 Mon Sep 17 00:00:00 2001 From: 101arrowz Date: Tue, 4 Jan 2022 23:16:27 -0800 Subject: [PATCH] decent UI --- .pwamanifestrc | 2 + package.json | 5 +- src-rs/image/document/detect.rs | 14 ++- src-rs/image/document/perspective.rs | 4 +- src-rs/image/grayscale.rs | 6 +- src-rs/image/mod.rs | 4 +- src-rs/lib.rs | 1 + src/camera.png | Bin 8550 -> 0 bytes src/camera.svg | 1 + src/done.svg | 1 + src/index.html | 30 +++++- src/index.ts | 134 ++++++++++++++++++--------- src/polyfill.ts | 5 + src/settings.svg | 1 + src/upload.svg | 1 + yarn.lock | 5 + 16 files changed, 153 insertions(+), 61 deletions(-) delete mode 100755 src/camera.png create mode 100755 src/camera.svg create mode 100755 src/done.svg create mode 100644 src/polyfill.ts create mode 100755 src/settings.svg create mode 100755 src/upload.svg diff --git a/.pwamanifestrc b/.pwamanifestrc index 5251951..323b58c 100644 --- a/.pwamanifestrc +++ b/.pwamanifestrc @@ -1,6 +1,8 @@ { "name": "Document Scanner", "disabled": true, + "theme": "black", + "display": "standalone", "iconGenOpts": { "baseIcon": "src/icon.png", "genFavicons": true diff --git a/package.json b/package.json index 1a1be78..e6634f2 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "typescript": "^4.4.4" }, "dependencies": { - "@parcel/service-worker": "^2.0.1" + "@parcel/service-worker": "^2.0.1", + "image-capture": "^0.4.0" } -} \ No newline at end of file +} diff --git a/src-rs/image/document/detect.rs b/src-rs/image/document/detect.rs index ca29eee..b0989cd 100644 --- a/src-rs/image/document/detect.rs +++ b/src-rs/image/document/detect.rs @@ -2,7 +2,6 @@ use core::cmp::Ordering; use super::super::Image; use alloc::vec::Vec; - use super::{ consts::{ANGS_PER_RAD, COS, GRADIENT_ERROR, HOUGH_MATCH_RATIO, MAX_ANG_ERROR, SIN}, Point, Quad, ScoredQuad, @@ -101,6 +100,15 @@ pub fn gradient_votes(source: &Image) -> GradientVotesResult { } } +// use wasm_bindgen::prelude::*; +// #[wasm_bindgen] +// #[derive(Clone, Copy)] +// pub struct Line { +// pub angle: u8, +// pub bin: usize, +// pub score: f32, +// } + #[derive(Clone, Copy)] pub struct Line { angle: u8, @@ -248,14 +256,14 @@ pub fn documents(result: &GradientVotesResult, lines: &[Line]) -> Vec impl Fn(Point) -> Point { let src_basis = basis_to_points(from); let dst_basis = basis_to_points(to); let proj = mul(dst_basis, adj(src_basis)); - return move |pt: Point| { + move |pt: Point| { let projected = mulv(proj, [pt.x, pt.y, 1.0]); Point { x: projected[0] / projected[2], y: projected[1] / projected[2], } - }; + } } pub fn perspective(source: &RGBAImage, quad: Quad, width: usize, height: usize) -> RGBAImage { diff --git a/src-rs/image/grayscale.rs b/src-rs/image/grayscale.rs index 298c9cb..cd3b649 100644 --- a/src-rs/image/grayscale.rs +++ b/src-rs/image/grayscale.rs @@ -13,9 +13,9 @@ pub fn grayscale(source: &RGBAImage) -> Image { data: source .chunks_exact(4) .map(|rgba| unsafe { - ((*rgba.get_unchecked(0) as f32) - + (*rgba.get_unchecked(1) as f32) - + (*rgba.get_unchecked(2)) as f32) * 0.00130718954248366 + (*rgba.get_unchecked(0) as f32) * 0.00116796875 + + (*rgba.get_unchecked(1) as f32) * 0.00229296875 + + (*rgba.get_unchecked(2) as f32) * 0.0004453125 }) .collect(), width, diff --git a/src-rs/image/mod.rs b/src-rs/image/mod.rs index 717a527..8dc5805 100644 --- a/src-rs/image/mod.rs +++ b/src-rs/image/mod.rs @@ -27,7 +27,7 @@ impl Image { // } pub fn quads(&self, mut tries: usize) -> Vec { let result = document::gradient_votes(self); - let mut threshold = 0.20; + let mut threshold = 0.01; while tries > 0 { tries -= 1; let mut edges = document::edges(&result, threshold); @@ -37,7 +37,7 @@ impl Image { } edges.sort_unstable_by(|a, b| b.cmp(a)); let documents = document::documents(&result, &edges); - if documents.len() > 0 { + if !documents.is_empty() { return documents; } threshold *= 0.5; diff --git a/src-rs/lib.rs b/src-rs/lib.rs index 40ee839..7270627 100644 --- a/src-rs/lib.rs +++ b/src-rs/lib.rs @@ -96,6 +96,7 @@ impl From for RGBAImage { } } +// use js_sys::Array; // #[wasm_bindgen] // pub fn find_edges(data: ImageData, threshold: f32) -> Array { // console_error_panic_hook::set_once(); diff --git a/src/camera.png b/src/camera.png deleted file mode 100755 index b24e26a403e7fa9adbca1d4c9d0f88376628ce38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8550 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 \ No newline at end of file diff --git a/src/done.svg b/src/done.svg new file mode 100755 index 0000000..5d721bb --- /dev/null +++ b/src/done.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/index.html b/src/index.html index 527f500..7889d06 100644 --- a/src/index.html +++ b/src/index.html @@ -2,6 +2,7 @@ + Document Scanner @@ -12,16 +13,39 @@ background: black; color: white; overflow: hidden; + width: 100vw; + height: 100vh; + } + #top-wrapper, #bottom-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100vw; } -
+
+
+
+ + +
+
- - +
+
+ + +
+ +
+ + +
+
diff --git a/src/index.ts b/src/index.ts index 688e0d8..ddb32eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,20 @@ import init, { find_document, extract_document, find_edges } from '../pkg'; import { toPDF } from './pdf'; import { getImage, download } from './io'; +import 'image-capture'; const root = document.getElementById('root') as HTMLDivElement; const preview = document.getElementById('preview') as HTMLVideoElement; const previewCrop = document.getElementById('preview-crop') as HTMLDivElement; +const bottomWrapper = document.getElementById('bottom-wrapper') as HTMLDivElement; +const topWrapper = document.getElementById('top-wrapper') as HTMLDivElement; +const selectWrapper = document.getElementById('camera-select-wrapper') as HTMLDivElement; const select = document.getElementById('camera-select') as HTMLSelectElement; -const shutter = document.getElementById('shutter') as HTMLSpanElement; +const uploadWrapper = document.getElementById('upload-wrapper') as HTMLDivElement; +const upload = document.getElementById('upload') as HTMLInputElement; +const shutter = document.getElementById('shutter') as HTMLImageElement; +const doneWrapper = document.getElementById('done-wrapper') as HTMLDivElement; +const done = document.getElementById('done') as HTMLButtonElement; type MaxRes = { width: number; @@ -17,6 +25,8 @@ type MaxRes = { let defaultMaxRes: Promise; let maxRes: Record | undefined> = {}; let wasmLoaded: Promise; +const pages: ImageData[] = []; + const log = (text: string) => { const el = document.createElement('div'); @@ -55,18 +65,69 @@ const getMaxRes = (device?: string) => { return prom; } -const mobile = /(iPad)|(iPhone)|(iPod)|(android)|(webOS)/i.test(navigator.userAgent); +const processImage = (img: ImageData) => { + // const cnv = document.createElement('canvas'); + // cnv.width = img.width; + // cnv.height = img.height; + // const ctx = cnv.getContext('2d')!; + // const diag = Math.hypot(img.width, img.height); + // let by = Math.min(img.width, img.height) / 360; + // if (by < 2) by = 1; + // ctx.putImageData(img, 0, 0); + // const edges = find_edges(img, 0.05); + // for (const { bin: b, angle: a, score } of edges) { + // const bin = Math.round(b) * by, angle = a * Math.PI / 256; + // ctx.strokeStyle = `rgba(255, 0, 0, ${score / edges[0].score})`; + // ctx.lineWidth = 5; + // const c = Math.cos(angle); + // const s = Math.sin(angle); + // const rho = ((bin << 1) - diag); + // const x = s * rho, y = c * rho; + // ctx.beginPath(); + // ctx.moveTo(x + c * 10000, y - s * 10000); + // ctx.lineTo(x - c * 10000, y + s * 10000); + // ctx.stroke(); + // } + // document.body.appendChild(cnv); + const doc = extract_document(img, find_document(img)!, 1224); + pages.push(doc); +} const startStream = async (device?: string) => { const maxRes = await getMaxRes(device); let aspectRatio = maxRes.width / maxRes.height; - if (aspectRatio < 1 / aspectRatio) { - aspectRatio = 1 / aspectRatio; - } const landscape = window.innerWidth > (window.innerHeight * aspectRatio); - root.style.flexDirection = landscape ? 'row' : 'column'; - const height = landscape ? window.innerHeight : Math.floor(Math.min(window.innerWidth * aspectRatio, window.innerHeight * 0.9)); - const width = landscape ? Math.floor(Math.min(window.innerHeight * aspectRatio, window.innerWidth * 0.9)) : window.innerWidth; + const height = landscape ? window.innerHeight : Math.floor(Math.min(window.innerWidth * aspectRatio, window.innerHeight * 0.84)); + const width = landscape ? Math.floor(Math.min(window.innerHeight * aspectRatio, window.innerWidth * 0.84)) : window.innerWidth; + const cssHeight = height + 'px'; + const cssWidth = width + 'px'; + previewCrop.style.width = previewCrop.style.minWidth = cssWidth; + previewCrop.style.height = previewCrop.style.minHeight = cssHeight; + root.style.width = window.innerWidth + 'px'; + root.style.height = window.innerHeight + 'px'; + if (landscape) { + preview.style.height = cssHeight; + preview.style.width = ''; + root.style.flexDirection = 'row'; + topWrapper.style.flexDirection = bottomWrapper.style.flexDirection = 'column'; + topWrapper.style.height = bottomWrapper.style.height = window.innerHeight + 'px'; + topWrapper.style.width = bottomWrapper.style.width = ''; + shutter.style.margin = doneWrapper.style.margin = uploadWrapper.style.margin = selectWrapper.style.margin = 0.02 * window.innerWidth + 'px'; + selectWrapper.style.width = selectWrapper.style.height = 0.03 * window.innerWidth + 'px'; + doneWrapper.style.width = doneWrapper.style.height = uploadWrapper.style.width = uploadWrapper.style.height = 0.035 * window.innerWidth + 'px'; + shutter.style.height = 0.05 * window.innerWidth + 'px'; + } else { + preview.style.height = ''; + preview.style.width = cssWidth; + root.style.flexDirection = 'column'; + topWrapper.style.flexDirection = bottomWrapper.style.flexDirection = 'row'; + topWrapper.style.height = bottomWrapper.style.height = ''; + topWrapper.style.width = bottomWrapper.style.width = window.innerWidth + 'px'; + shutter.style.margin = doneWrapper.style.margin = uploadWrapper.style.margin = selectWrapper.style.margin = 0.02 * window.innerHeight + 'px'; + selectWrapper.style.width = selectWrapper.style.height = 0.03 * window.innerHeight + 'px'; + doneWrapper.style.width = doneWrapper.style.height = uploadWrapper.style.width = uploadWrapper.style.height = shutter.style.height = 0.035 * window.innerHeight + 'px'; + shutter.style.height = 0.05 * window.innerHeight + 'px'; + } const constraints: MediaTrackConstraints = { width: maxRes.width, height: maxRes.height, @@ -78,47 +139,11 @@ const startStream = async (device?: string) => { }); const videoTrack = stream.getVideoTracks()[0]; preview.srcObject = stream; - const settings = videoTrack.getSettings() - const cssHeight = height + 'px'; - const cssWidth = width + 'px'; - if (landscape) { - preview.style.height = cssHeight; - preview.style.width = ''; - } else { - preview.style.height = ''; - preview.style.width = cssWidth; - } - previewCrop.style.width = previewCrop.style.minWidth = cssWidth; - previewCrop.style.height = previewCrop.style.minHeight = cssHeight; const cap = new ImageCapture(videoTrack); const onShutterClick = async () => { await wasmLoaded; const photo = await cap.takePhoto(); - const img = await getImage(photo); - // const cnv = document.createElement('canvas'); - // cnv.width = img.width; - // cnv.height = img.height; - // const ctx = cnv.getContext('2d')!; - // const diag = Math.hypot(img.width, img.height); - // let by = Math.min(img.width, img.height) / 360; - // if (by < 2) by = 1; - // ctx.putImageData(img, 0, 0); - // for (const { bin: b, angle: a, score: s } of find_edges(img, 0.1)) { - // const bin = Math.round(b) * by, angle = a * Math.PI / 256; - // ctx.strokeStyle = `rgba(255, 0, 0, ${1})`; - // ctx.lineWidth = 3; - // const c = Math.cos(angle); - // const s = Math.sin(angle); - // const rho = ((bin << 1) - diag); - // const x = s * rho, y = c * rho; - // ctx.beginPath(); - // ctx.moveTo(x + c * 10000, y - s * 10000); - // ctx.lineTo(x - c * 10000, y + s * 10000); - // ctx.stroke(); - // } - // document.body.appendChild(cnv); - const doc = extract_document(img, find_document(img)!, 1224); - download(new Blob([await toPDF([doc])]), 'out.pdf') + processImage(await getImage(photo)); } shutter.addEventListener('click', onShutterClick); return { @@ -138,16 +163,24 @@ const startStream = async (device?: string) => { const onLoad = async () => { wasmLoaded = init().then(); let stream = await startStream(localStorage.getItem('defaultDevice')!); + const updateBold = () => { + for (const option of select.options) { + option.style.fontWeight = ''; + } + select.selectedOptions[0].style.fontWeight = 'bold'; + } for (const device of await navigator.mediaDevices.enumerateDevices()) { if (device.kind == 'videoinput') { const option = document.createElement('option'); option.value = device.deviceId; - option.innerText = device.label; + option.label = device.label; select.appendChild(option); } } select.value = stream.deviceId; + updateBold(); const onUpdate = async () => { + updateBold(); stream.close(); select.disabled = true; localStorage.setItem('defaultDevice', select.value); @@ -160,6 +193,15 @@ const onLoad = async () => { clearTimeout(rst); rst = setTimeout(onUpdate, 250) as unknown as number; }; + upload.onchange = async () => { + for (const file of upload.files!) { + processImage(await getImage(file)); + } + }; + done.onclick = async () => { + download(new Blob([await toPDF(pages)]), 'out.pdf') + pages.length = 0; + } } onLoad(); diff --git a/src/polyfill.ts b/src/polyfill.ts new file mode 100644 index 0000000..ac3ebb4 --- /dev/null +++ b/src/polyfill.ts @@ -0,0 +1,5 @@ +if (typeof ImageCapture == 'undefined') { + Object.defineProperty(window, 'ImageCapture', class ImageCapture { + + }); +} \ No newline at end of file diff --git a/src/settings.svg b/src/settings.svg new file mode 100755 index 0000000..7074fa8 --- /dev/null +++ b/src/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/upload.svg b/src/upload.svg new file mode 100755 index 0000000..2a16106 --- /dev/null +++ b/src/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index af7268f..92038ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2608,6 +2608,11 @@ ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb" integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ== +image-capture@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/image-capture/-/image-capture-0.4.0.tgz#67b96608d0b58ecb1337ee335e4492733f6c11ee" + integrity sha512-6RWTfqC4ij0AldG+6sQ51XSHTSbwfqMSjVl1GtwNBzbW4UrcfGZeB1Kn749BccvtLb04g5+jSTf1D7q3qHcxpA== + import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"