source: main/trunk/model-interfaces-dev/atea/ocr/src/components/MainPage.vue@ 35956

Last change on this file since 35956 was 35956, checked in by cstephen, 2 years ago

Refactor editor components to editor folder, fix thresholded image display

File size: 13.0 KB
Line 
1<script>
2import { mapState } from "vuex";
3import EditPage from "./editor/EditPage.vue";
4import OcrImageDisplay from "./OcrImageDisplay.vue"
5import { ModalController } from "./ModalDialog.vue"
6import { SnackController } from "./Snackbar.vue"
7import OcrService, { OcrOptions } from "../js/OcrService"
8import Util, { log } from "../js/Util";
9
10const ocrService = new OcrService();
11
12export default {
13 name: "MainPage",
14 components: {
15 EditPage,
16 OcrImageDisplay
17 },
18 data() {
19 return {
20 imageType: null,
21 /** @type {ArrayBuffer} */
22 imageBuffer: null,
23 imageUrl: null,
24
25 thresholdedImageUrl: null,
26 showThresholdedImage: false,
27
28 ocrInProgress: false,
29 /** @type {OcrResult} */
30 ocrResult: null,
31
32 showEditor: false,
33 ocrTextEdited: false,
34 enableMagnifier: false,
35 magnifierZoom: 1.5,
36 enableSpellcheck: false
37 }
38 },
39 computed: {
40 ...mapState({
41 translations: state => state.translations,
42 isValidOcrResult() {
43 return this.ocrResult !== null && this.ocrResult.length !== 0;
44 }
45 })
46 },
47 watch: {
48 imageBuffer(newValue) {
49 if (this.imageUrl !== null) {
50 URL.revokeObjectURL(this.imageUrl);
51 }
52
53 if (newValue === null) {
54 return;
55 }
56
57 const arrayView = new Uint8Array(newValue);
58 const blob = new Blob([ arrayView ], { type: this.imageType });
59 this.imageUrl = URL.createObjectURL(blob);
60 },
61 enableMagnifier(newValue) {
62 localStorage.setItem("enableMagnifier", newValue);
63 },
64 magnifierZoom(newValue) {
65 localStorage.setItem("magnifierZoom", newValue);
66 this.enableMagnifier = true;
67 },
68 enableSpellcheck(newValue) {
69 localStorage.setItem("enableSpellcheck", newValue);
70 }
71 },
72 methods: {
73 uploadFile() {
74 this.$refs.fileInput.click();
75 },
76 async onFilesChanged() {
77 /** @type {File[]} */
78 const files = this.$refs.fileInput.files;
79 if (files === null || files === undefined || files.length !== 1) {
80 return;
81 }
82
83 this.imageBuffer = await files[0].arrayBuffer();
84 this.imageType = files[0].type;
85 },
86
87 async doOcr() {
88 try {
89 if (this.ocrTextEdited) {
90 this.confirmOcr();
91 return;
92 }
93
94 this.ocrInProgress = true;
95
96 const longTimeout = setTimeout(() => {
97 SnackController.addSnack(this.translations.get("MainPage_DelayedOcrMessage"), 8000);
98 }, 10000)
99
100 const arrayView = new Uint8Array(this.imageBuffer);
101 const imageBlob = new Blob([ arrayView ], { type: this.imageType });
102
103 const result = await ocrService.run([
104 {
105 image: imageBlob,
106 fileName: "file.png",
107 options: new OcrOptions(false)
108 }
109 ]);
110
111 clearTimeout(longTimeout);
112 this.ocrResult = result[0].text;
113 this.ocrTextEdited = false;
114
115 const thresholdedImageBlob = await ocrService.getThresholdedImage(result[0].thresholdedImageKey);
116
117 if (this.thresholdedImageUrl !== null) {
118 URL.revokeObjectURL(this.thresholdedImageUrl);
119 }
120
121 this.thresholdedImageUrl = URL.createObjectURL(thresholdedImageBlob);
122 }
123 catch (ex) {
124 // TODO: Display error
125 log("Failed to perform OCR", "error");
126 log(ex, "error");
127 }
128 finally {
129 this.ocrInProgress = false;
130 }
131 },
132 confirmOcr() {
133 ModalController.open(
134 this.translations.get("OCREditedModal_Title"),
135 this.translations.get("OCREditedModal_Description"),
136 [
137 this.translations.get("OCREditedModal_ButtonContinue"),
138 this.translations.get("OCREditedModal_ButtonCancel")
139 ],
140 async (buttonName) => {
141 if (buttonName === this.translations.get("OCREditedModal_ButtonContinue")) {
142 this.ocrTextEdited = false;
143 await this.doOcr();
144 }
145 }
146 );
147 },
148
149 onEditorCloseRequested() {
150 this.showEditor = false;
151 },
152 /**
153 * Called when the editor is saved in order to update the stored image buffer.
154 * @param {Buffer} newBuffer The updated image buffer.
155 * @param {String} newType The updated MIME type of the image buffer.
156 */
157 onEditorSave(newBuffer, newType) {
158 this.imageType = newType;
159 this.imageBuffer = newBuffer.buffer;
160
161 this.onEditorCloseRequested();
162 },
163
164 reset(hard) {
165 if (!hard) {
166 ModalController.open(
167 this.translations.get("NewImageModal_Title"),
168 this.translations.get("NewImageModal_Description"),
169 [
170 this.translations.get("NewImageModal_ButtonContinue"),
171 this.translations.get("NewImageModal_ButtonCancel")
172 ],
173 (buttonName) => {
174 if (buttonName === this.translations.get("NewImageModal_ButtonContinue")) {
175 this.reset(true);
176 }
177 }
178 );
179
180 return;
181 }
182
183 if (this.imageUrl) {
184 URL.revokeObjectURL(this.imageUrl);
185 this.imageUrl = null;
186 }
187
188 if (this.thresholdedImageUrl) {
189 URL.revokeObjectURL(this.thresholdedImageUrl);
190 this.thresholdedImageUrl = null;
191 }
192
193 this.ocrInProgress = false;
194 this.ocrResult = null;
195 this.showEditor = false;
196 this.ocrTextEdited = false;
197 this.$refs.fileInput.value = "";
198 },
199
200 async download() {
201 const blob = new Blob([ this.ocrResult ], { type: "text/plain;charset=utf-8" });
202 const date = new Date();
203 const fileName = `ocr-${date.getHours()}${date.getMinutes()}${date.getSeconds()}.txt`;
204
205 const { saveAs } = await import("file-saver");
206 saveAs(blob, fileName);
207 }
208 },
209 beforeMount() {
210 this.enableMagnifier = localStorage.getItem("enableMagnifier") === "true";
211 this.magnifierZoom = parseFloat(localStorage.getItem("magnifierZoom") ?? "1.5");
212
213 const enableSpellcheck = localStorage.getItem("enableSpellcheck");
214 if (enableSpellcheck) {
215 this.enableSpellcheck = enableSpellcheck === "true";
216 }
217 else {
218 this.enableSpellcheck = Util.getNavigatorLanguage().startsWith("mi");
219 }
220 }
221}
222</script>
223
224<template>
225<div class="main-page-root">
226 <edit-page v-if="showEditor" class="image-editor" :src="imageUrl" :imageBuffer="imageBuffer"
227 @closeAndDiscard="onEditorCloseRequested" @closeAndSave="onEditorSave" />
228
229 <div class="paper root-container" :class="{ 'root-container-image-state': imageUrl !== null }">
230 <div v-if="imageUrl === null" class="upload-area" @click="uploadFile">
231 <span class="heading1">{{ translations.get("Title") }}</span>
232 <span class="material-icons mdi-xl">upload_file</span>
233 <span>{{ translations.get("MainPage_Upload") }}</span>
234 </div>
235
236 <div v-if="imageUrl !== null" class="main-area">
237 <div class="controls">
238 <button class="btn-primary" @click="showEditor = true" :disabled="ocrInProgress">
239 <span class="material-icons">edit</span>
240 <span>{{ translations.get("MainPage_EditImage") }}</span>
241 </button>
242
243 <button class="btn-primary" @click="doOcr" :disabled="ocrInProgress">
244 <span class="material-icons">play_arrow</span>
245 <span>{{ translations.get("MainPage_PerformOCR") }}</span>
246 </button>
247
248 <button class="btn-primary" @click="download" :disabled="!isValidOcrResult">
249 <span class="material-icons">download</span>
250 <span>{{ translations.get("MainPage_Download") }}</span>
251 </button>
252
253 <button class="btn-primary" @click="reset(false)" :disabled="ocrInProgress">
254 <span class="material-icons">restart_alt</span>
255 <span>{{ translations.get("MainPage_NewImage") }}</span>
256 </button>
257 </div>
258
259 <div class="progress-bar-container ocr-progress">
260 <div v-if="ocrInProgress" class="progress-bar-value progress-bar-indeterminate" />
261 </div>
262
263 <div class="content-controls">
264 <div>
265 <div class="flex-h">
266 <input id="chk-enable-magnifier" type="checkbox" v-model="enableMagnifier" />
267 <label for="chk-enable-magnifier">{{ translations.get('MainPage_EnableMagnifier') }}</label>
268 </div>
269
270 <div class="flex-h" style="margin-top: 0.5em">
271 <span>1.5x</span>
272 <input type="range" min="1.5" max="3" step="0.1" v-model.number="magnifierZoom" />
273 <span>3x</span>
274 </div>
275 </div>
276
277 <div style="max-width: 450px; word-wrap: normal;">
278 <div class="flex-h">
279 <input id="chk-show-thresholded-image" type="checkbox" v-model="showThresholdedImage" :disabled="!ocrResult" />
280 <label for="chk-show-thresholded-image">{{ translations.get('MainPage_ShowThresholdedImage') }}</label>
281 </div>
282
283 <span class="body2">
284 {{ translations.get('MainPage_ThresholdedImageDescription') }}
285 </span>
286 </div>
287
288 <div class="flex-h">
289 <input id="chk-enable-spellcheck" type="checkbox" v-model="enableSpellcheck" />
290 <label for="chk-enable-spellcheck">{{ translations.get('MainPage_EnableSpellcheck') }}</label>
291 </div>
292 </div>
293
294 <div class="main-content-columns">
295 <ocr-image-display
296 :imageUrl="imageUrl"
297 :thresholdedImageUrl="thresholdedImageUrl"
298 :enableMagnifier="enableMagnifier"
299 :magnifierZoom="magnifierZoom"
300 :showThresholdedImage="showThresholdedImage" />
301
302 <textarea
303 class="text-container"
304 v-model="ocrResult"
305 :placeholder="translations.get('MainPage_OCRHint')"
306 @input="ocrTextEdited = true"
307 :disabled="!isValidOcrResult || ocrInProgress"
308 autocomplete="off"
309 :spellcheck="enableSpellcheck"/>
310 </div>
311 </div>
312 </div>
313
314 <input ref="fileInput" type="file" @input="onFilesChanged" class="hidden"
315 accept="image/png,image/jpeg,image/gif,image/bmp,image/tiff,image/webp" />
316</div>
317
318</template>
319
320<style lang="scss" scoped>
321.main-page-root {
322 display: flex;
323 align-items: center;
324 justify-content: center;
325}
326
327.root-container {
328 padding: 1em;
329 transition-duration: var(--transition-duration);
330}
331
332.root-container-image-state {
333 height: 100%;
334 width: 100%;
335
336 overflow: visible;
337}
338
339.upload-area {
340 display: grid;
341 grid-template-columns: auto auto;
342 grid-template-rows: auto 1fr;
343 align-items: center;
344 justify-items: center;
345
346 padding: 2em;
347 gap: 1em;
348
349 font-size: 2rem;
350 cursor: pointer;
351 border: 3px dashed var(--bg-color);
352 border-radius: var(--border-radius);
353
354 .heading1 {
355 grid-column-start: span 2;
356 color: #666;
357 }
358}
359
360.hidden {
361 display: none;
362}
363
364.image-editor {
365 position: absolute;
366 height: 100%;
367 width: 100%;
368 top: 0;
369 left: 0;
370 z-index: 9;
371}
372
373.main-area {
374 display: flex;
375 flex-direction: column;
376 height: 100%;
377
378 gap: 1rem;
379 align-items: center;
380 justify-items: center;
381
382 position: relative;
383 overflow: visible;
384
385 .text-container {
386 height: 100%;
387 width: 100%;
388 }
389}
390
391.controls {
392 display: flex;
393 gap: 1em;
394}
395
396.content-controls {
397 display: flex;
398 align-items: flex-start;
399 gap: 1em;
400}
401
402.main-content-columns {
403 display: grid;
404 grid-template-columns: 1fr 1fr;
405 grid-template-rows: 1fr;
406 flex-grow: 1;
407 width: 100%;
408
409 gap: 1em;
410 align-items: center;
411 justify-items: center;
412}
413</style>
Note: See TracBrowser for help on using the repository browser.