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

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

Refactor OcrImageDisplay to act as a magnification container for an image

File size: 13.1 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 v-if="!showThresholdedImage"
297 :imageUrl="imageUrl"
298 :enableMagnifier="enableMagnifier"
299 :magnifierZoom="magnifierZoom" />
300
301 <ocr-image-display
302 v-else
303 :imageUrl="thresholdedImageUrl"
304 :enableMagnifier="enableMagnifier"
305 :magnifierZoom="magnifierZoom" />
306
307 <textarea
308 class="text-container"
309 v-model="ocrResult"
310 :placeholder="translations.get('MainPage_OCRHint')"
311 @input="ocrTextEdited = true"
312 :disabled="!isValidOcrResult || ocrInProgress"
313 autocomplete="off"
314 :spellcheck="enableSpellcheck"/>
315 </div>
316 </div>
317 </div>
318
319 <input ref="fileInput" type="file" @input="onFilesChanged" class="hidden"
320 accept="image/png,image/jpeg,image/gif,image/bmp,image/tiff,image/webp" />
321</div>
322
323</template>
324
325<style lang="scss" scoped>
326.main-page-root {
327 display: flex;
328 align-items: center;
329 justify-content: center;
330}
331
332.root-container {
333 padding: 1em;
334 transition-duration: var(--transition-duration);
335}
336
337.root-container-image-state {
338 height: 100%;
339 width: 100%;
340
341 overflow: visible;
342}
343
344.upload-area {
345 display: grid;
346 grid-template-columns: auto auto;
347 grid-template-rows: auto 1fr;
348 align-items: center;
349 justify-items: center;
350
351 padding: 2em;
352 gap: 1em;
353
354 font-size: 2rem;
355 cursor: pointer;
356 border: 3px dashed var(--bg-color);
357 border-radius: var(--border-radius);
358
359 .heading1 {
360 grid-column-start: span 2;
361 color: #666;
362 }
363}
364
365.hidden {
366 display: none;
367}
368
369.image-editor {
370 position: absolute;
371 height: 100%;
372 width: 100%;
373 top: 0;
374 left: 0;
375 z-index: 9;
376}
377
378.main-area {
379 display: flex;
380 flex-direction: column;
381 height: 100%;
382
383 gap: 1rem;
384 align-items: center;
385 justify-items: center;
386
387 position: relative;
388 overflow: visible;
389
390 .text-container {
391 height: 100%;
392 width: 100%;
393 }
394}
395
396.controls {
397 display: flex;
398 gap: 1em;
399}
400
401.content-controls {
402 display: flex;
403 align-items: flex-start;
404 gap: 1em;
405}
406
407.main-content-columns {
408 display: grid;
409 grid-template-columns: 1fr 1fr;
410 grid-template-rows: 1fr;
411 flex-grow: 1;
412 width: 100%;
413
414 gap: 1em;
415 align-items: center;
416 justify-items: center;
417}
418</style>
Note: See TracBrowser for help on using the repository browser.