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

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

Lazy-load jimp to improve startup time.
Add modal dialog.
Confirm with user before resetting.
npm: Add pdf.js

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