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

Last change on this file since 35752 was 35752, checked in by cstephen, 13 months ago

Implement editing and downloading of the OCR result.

File size: 9.4 KB
Line 
1<template>
2<div class="main-page-root">
3 <modal-dialog v-if="modal.show" :title="modal.title" :description="modal.description" :buttons="modal.buttons" @close="onModalClose" />
4 <edit-page v-if="showEditor" class="image-editor" :src="imageUrl" :imageBuffer="imageBuffer"
5 @closeAndDiscard="onEditorCloseRequested" @closeAndSave="onEditorSave" />
6
7 <div class="paper root-container" :class="{ 'root-container-image-state': imageUrl !== null }">
8 <div v-if="imageUrl === null" class="upload-area" @click="uploadFile">
9 <span class="heading1">{{ translations.get("Title") }}</span>
10 <span class="material-icons mdi-xl">upload_file</span>
11 <span>{{ translations.get("MainPage_Upload") }}</span>
12 </div>
13
14 <div v-if="imageUrl !== null" class="image-area">
15 <div class="controls">
16 <button class="btn-primary" @click="doOcr" :disabled="ocrInProgress">
17 <span class="material-icons">play_arrow</span>
18 <span>{{ translations.get("MainPage_PerformOCR") }}</span>
19 </button>
20
21 <button class="btn-primary" @click="showEditor = true" :disabled="ocrInProgress">
22 <span class="material-icons">edit</span>
23 <span>{{ translations.get("MainPage_EditImage") }}</span>
24 </button>
25
26 <button class="btn-primary" @click="download" :disabled="ocrResult === null || ocrResult.length === 0">
27 <span class="material-icons">download</span>
28 <span>{{ translations.get("MainPage_Download") }}</span>
29 </button>
30
31 <button class="btn-primary" @click="reset(false)" :disabled="ocrInProgress">
32 <span class="material-icons">restart_alt</span>
33 <span>{{ translations.get("MainPage_NewImage") }}</span>
34 </button>
35 </div>
36
37 <div class="progress-bar-container ocr-progress">
38 <div v-if="ocrInProgress" class="progress-bar-value progress-bar-indeterminate" />
39 </div>
40
41 <img class="image-display" :src="imageUrl" />
42
43 <textarea class="text-container" v-model="ocrResult" :placeholder="translations.get('MainPage_OCRHint')"
44 @input="modalChecks.ocrTextEdited = true" />
45 </div>
46 </div>
47
48 <input ref="fileInput" type="file" @input="onFilesChanged" class="hidden"
49 accept="image/png,image/jpeg,image/gif,image/bmp,image/tiff,image/webp,application/pdf" />
50</div>
51
52</template>
53
54<style lang="scss" scoped>
55.main-page-root {
56 display: flex;
57 align-items: center;
58 justify-content: center;
59}
60
61.root-container {
62 padding: 1em;
63 transition-duration: var(--transition-duration);
64}
65
66.root-container-image-state {
67 width: calc(100% - 3em);
68 height: calc(100vh - 3em);
69 max-height: calc(100vh - 3em);
70}
71
72.upload-area {
73 display: grid;
74 grid-template-columns: auto auto;
75 grid-template-rows: auto 1fr;
76 align-items: center;
77 justify-items: center;
78
79 padding: 2em;
80 gap: 1em;
81
82 font-size: 2rem;
83 cursor: pointer;
84 border: 3px dashed var(--bg-color);
85 border-radius: var(--border-radius);
86
87 .heading1 {
88 grid-column-start: span 2;
89 color: #666;
90 }
91}
92
93.hidden {
94 display: none;
95}
96
97.image-editor {
98 position: absolute;
99 height: 100%;
100 width: 100%;
101 top: 0;
102 left: 0;
103 z-index: 9;
104}
105
106.image-area {
107 display: grid;
108 grid-template-columns: 1fr 1fr;
109 grid-template-rows: auto auto 1fr;
110 height: 100%;
111
112 gap: 1rem;
113 align-items: center;
114 justify-items: center;
115
116 position: relative;
117 overflow: hidden;
118
119 .text-container {
120 height: 100%;
121 width: 100%;
122 }
123}
124
125.image-display {
126 border: 1px solid #444;
127 max-width: 100%;
128 max-height: 80vh;
129 object-fit: contain;
130}
131
132.controls {
133 display: flex;
134 gap: 1em;
135 grid-column-start: span 2;
136}
137
138.ocr-progress {
139 grid-column-start: span 2;
140}
141</style>
142
143<script>
144import { mapState } from "vuex";
145import EditPage from "./EditPage.vue";
146import ModalDialog from "./ModalDialog.vue"
147import OcrService, { OcrOptions } from "../js/OcrService"
148import { log } from "../js/Util";
149
150const ocrService = new OcrService();
151
152export default {
153 name: "MainPage",
154 components: {
155 EditPage,
156 ModalDialog
157 },
158 data() {
159 return {
160 imageType: null,
161 /** @type {ArrayBuffer} */
162 imageBuffer: null,
163 imageUrl: null,
164 ocrInProgress: false,
165 ocrResult: null,
166 showEditor: false,
167 modal: {
168 show: false,
169 title: "",
170 description: "",
171 buttons: []
172 },
173 modalChecks: {
174 ocrTextEdited: false,
175 pendingReset: false
176 }
177 }
178 },
179 computed: {
180 ...mapState({
181 translations: state => state.translations
182 })
183 },
184 watch: {
185 imageBuffer(newValue) {
186 if (this.imageUrl !== null) {
187 URL.revokeObjectURL(this.imageUrl);
188 }
189
190 if (newValue === null) {
191 return;
192 }
193
194 const arrayView = new Uint8Array(newValue);
195 const blob = new Blob([ arrayView ]);
196 this.imageUrl = URL.createObjectURL(blob);
197 }
198 },
199 methods: {
200 uploadFile() {
201 this.$refs.fileInput.click();
202 },
203 async onFilesChanged() {
204 /** @type {File[]} */
205 const files = this.$refs.fileInput.files;
206 if (files === null || files === undefined || files.length !== 1) {
207 return;
208 }
209
210 this.imageBuffer = await files[0].arrayBuffer();
211 this.imageType = files[0].type;
212 },
213
214 async doOcr() {
215 if (this.modalChecks.ocrTextEdited) {
216 this.modal.title = this.translations.get("OCREditedModal_Title");
217 this.modal.description = this.translations.get("OCREditedModal_Description");
218 this.modal.buttons = [
219 this.translations.get("OCREditedModal_ButtonContinue"),
220 this.translations.get("OCREditedModal_ButtonCancel")
221 ];
222 this.modal.show = true;
223
224 return;
225 }
226
227 try {
228 this.ocrInProgress = true;
229
230 const arrayView = new Uint8Array(this.imageBuffer);
231 const imageBlob = new Blob([ arrayView ], { type: this.imageType });
232
233 const result = await ocrService.run([
234 {
235 image: imageBlob,
236 fileName: "file.png",
237 options: new OcrOptions(false)
238 }
239 ]);
240
241 this.ocrResult = result[0].text;
242 this.modalChecks.ocrTextEdited = false;
243 }
244 catch (ex) {
245 // TODO: Display error
246 log("Failed to perform OCR", "error");
247 log(ex, "error");
248 }
249 finally {
250 this.ocrInProgress = false;
251 }
252 },
253
254 onEditorCloseRequested() {
255 this.showEditor = false;
256 },
257 /**
258 * Called when the editor is saved in order to update the stored image buffer.
259 * @param {Buffer} newBuffer The updated image buffer.
260 * @param {String} newType The updated MIME type of the image buffer.
261 */
262 onEditorSave(newBuffer, newType) {
263 this.imageBuffer = newBuffer.buffer;
264 this.imageType = newType;
265
266 this.onEditorCloseRequested();
267 },
268
269 reset(hard) {
270 if (!hard) {
271 this.modalChecks.pendingReset = true;
272
273 this.modal.title = this.translations.get("NewImageModal_Title");
274 this.modal.description = this.translations.get("NewImageModal_Description");
275 this.modal.buttons = [
276 this.translations.get("NewImageModal_ButtonContinue"),
277 this.translations.get("NewImageModal_ButtonCancel")
278 ];
279 this.modal.show = true;
280
281 return;
282 }
283
284 URL.revokeObjectURL(this.imageUrl);
285 this.imageUrl = null;
286 this.ocrInProgress = false;
287 this.ocrResult = null;
288 this.showEditor = false;
289 this.$refs.fileInput.value = "";
290 },
291
292 async onModalClose(button) {
293 this.modal.show = false;
294
295 if (this.modalChecks.pendingReset) {
296 console.log("reset is indeed pending");
297 if (button === "Continue") {
298 this.reset(true);
299 }
300
301 this.modalChecks.pendingReset = false;
302 }
303 else if (this.modalChecks.ocrTextEdited) {
304 if (button === "Continue") {
305 this.modalChecks.ocrTextEdited = false;
306 await this.doOcr();
307 }
308 }
309 },
310
311 async download() {
312 const blob = new Blob([ this.ocrResult ], { type: "text/plain;charset=utf-8" });
313 const date = new Date();
314 const fileName = `ocr-${date.getHours()}${date.getMinutes()}${date.getSeconds()}.txt`;
315
316 const { saveAs } = await import("file-saver");
317 saveAs(blob, fileName);
318 }
319 }
320}
321</script>
Note: See TracBrowser for help on using the repository browser.