Skip to content

Commit

Permalink
decent UI
Browse files Browse the repository at this point in the history
  • Loading branch information
101arrowz committed Jan 5, 2022
1 parent c0bcb2a commit 5638a20
Show file tree
Hide file tree
Showing 16 changed files with 153 additions and 61 deletions.
2 changes: 2 additions & 0 deletions .pwamanifestrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"name": "Document Scanner",
"disabled": true,
"theme": "black",
"display": "standalone",
"iconGenOpts": {
"baseIcon": "src/icon.png",
"genFavicons": true
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
14 changes: 11 additions & 3 deletions src-rs/image/document/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -248,14 +256,14 @@ pub fn documents(result: &GradientVotesResult, lines: &[Line]) -> Vec<ScoredQuad
}
}

(score * ((dx - dy) as f32).powf(-0.6)).max(0.0)
(score * ((dx - dy) as f32).powf(-0.3)).max(0.0)
};
let scored_quad = |quad: Quad, l1: Line, l2: Line, l3: Line, l4: Line| {
let edge_total = score_between(quad.a, quad.b)
+ score_between(quad.b, quad.c)
+ score_between(quad.c, quad.d)
+ score_between(quad.d, quad.a);
let edge_score = edge_total * edge_total;
let edge_score = edge_total.powf(3.0);

let e12 = right_err(l1, l2);
let e23 = right_err(l2, l3);
Expand Down
4 changes: 2 additions & 2 deletions src-rs/image/document/perspective.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ fn create_projector(from: Quad, to: Quad) -> 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 {
Expand Down
6 changes: 3 additions & 3 deletions src-rs/image/grayscale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src-rs/image/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl Image {
// }
pub fn quads(&self, mut tries: usize) -> Vec<ScoredQuad> {
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);
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src-rs/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ impl From<ImageData> for RGBAImage {
}
}

// use js_sys::Array;
// #[wasm_bindgen]
// pub fn find_edges(data: ImageData, threshold: f32) -> Array {
// console_error_panic_hook::set_once();
Expand Down
Binary file removed src/camera.png
Binary file not shown.
1 change: 1 addition & 0 deletions src/camera.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/done.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 27 additions & 3 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Scanner</title>
Expand All @@ -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;
}
</style>
</head>
<body>
<div id="root" style="display:flex; flex-direction: row; justify-content: center; align-items: center;">
<div id="root" style="display: flex; flex-direction: column; justify-content: space-between; align-items: center;">
<div id="top-wrapper" style="align-items: flex-start;">
<div id="camera-select-wrapper" style="position: relative; height: 3vh; width: 3vh;">
<select id="camera-select" style="position: absolute; top: 0; left: 0; height: 100%; width: 100%; opacity: 0; z-index: 2; cursor: pointer;"></select>
<img style="position: absolute; top: 0; left: 0; cursor: pointer; height: 100%; z-index: 1" src="settings.svg"></img>
</div>
</div>
<div id="preview-crop" style="display: flex; justify-content: center; align-items: center; overflow: hidden;">
<video id="preview" autoplay playsinline></video>
</div>
<select id="camera-select"></select>
<img id="shutter" style="cursor: pointer; height: 5vmax;" src="camera.png"></img>
<div id="bottom-wrapper" style="align-items: center;">
<div id="upload-wrapper" style="position: relative; height: 5vh; width: 5vh;">
<input id="upload" type="file" accept="image/*" multiple style="position: absolute; top: 0; left: 0; height: 100%; width: 100%; opacity: 0; z-index: 2; cursor: pointer;"></input>
<img style="position: absolute; top: 0; left: 0; cursor: pointer; height: 100%; z-index: 1" src="upload.svg"></img>
</div>
<img id="shutter" style="cursor: pointer; height: 5vh;" src="camera.svg"></img>
<div id="done-wrapper" style="position: relative; height: 5vh; width: 5vh;">
<button id="done" style="position: absolute; top: 0; left: 0; height: 100%; width: 100%; opacity: 0; z-index: 2; cursor: pointer;"></button>
<img style="position: absolute; top: 0; left: 0; cursor: pointer; height: 100%; z-index: 1" src="done.svg"></img>
</div>
</div>
</div>
<script type="module" src="index.ts"></script>
</body>
Expand Down
134 changes: 88 additions & 46 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,6 +25,8 @@ type MaxRes = {
let defaultMaxRes: Promise<MaxRes>;
let maxRes: Record<string, Promise<MaxRes> | undefined> = {};
let wasmLoaded: Promise<void>;
const pages: ImageData[] = [];


const log = (text: string) => {
const el = document.createElement('div');
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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);
Expand All @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions src/polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if (typeof ImageCapture == 'undefined') {
Object.defineProperty(window, 'ImageCapture', class ImageCapture {

});
}
1 change: 1 addition & 0 deletions src/settings.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/upload.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 5638a20

Please sign in to comment.