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
RevLine 
[35734]1<template>
[35743]2<div class="main-page-root">
[35751]3 <modal-dialog v-if="modal.show" :title="modal.title" :description="modal.description" :buttons="modal.buttons" @close="onModalClose" />
[35746]4 <edit-page v-if="showEditor" class="image-editor" :src="imageUrl" :imageBuffer="imageBuffer"
5 @closeAndDiscard="onEditorCloseRequested" @closeAndSave="onEditorSave" />
[35734]6
[35746]7 <div class="paper root-container" :class="{ 'root-container-image-state': imageUrl !== null }">
8 <div v-if="imageUrl === null" class="upload-area" @click="uploadFile">
[35743]9 <span class="heading1">{{ translations.get("Title") }}</span>
10 <span class="material-icons mdi-xl">upload_file</span>
[35747]11 <span>{{ translations.get("MainPage_Upload") }}</span>
[35743]12 </div>
[35734]13
[35746]14 <div v-if="imageUrl !== null" class="image-area">
[35743]15 <div class="controls">
16 <button class="btn-primary" @click="doOcr" :disabled="ocrInProgress">
17 <span class="material-icons">play_arrow</span>
[35747]18 <span>{{ translations.get("MainPage_PerformOCR") }}</span>
[35743]19 </button>
[35734]20
[35746]21 <button class="btn-primary" @click="showEditor = true" :disabled="ocrInProgress">
[35743]22 <span class="material-icons">edit</span>
[35747]23 <span>{{ translations.get("MainPage_EditImage") }}</span>
[35743]24 </button>
[35734]25
[35751]26 <button class="btn-primary" @click="reset(false)" :disabled="ocrInProgress">
[35743]27 <span class="material-icons">restart_alt</span>
[35747]28 <span>{{ translations.get("MainPage_NewImage") }}</span>
[35743]29 </button>
[35748]30 </div>
[35734]31
[35748]32 <div class="progress-bar-container ocr-progress">
33 <div v-if="ocrInProgress" class="progress-bar-value progress-bar-indeterminate" />
[35743]34 </div>
35
36 <img class="image-display" :src="imageUrl" />
37
[35734]38 <div class="text-container">
[35748]39 <span v-if="ocrResult === null && !ocrInProgress" class="text-placeholder">{{ translations.get("MainPage_OCRHint") }}</span>
[35743]40 <pre>{{ ocrResult }}</pre>
[35734]41 </div>
42 </div>
43 </div>
44
[35743]45 <input ref="fileInput" type="file" @input="onFilesChanged" class="hidden"
[35734]46 accept="image/png,image/jpeg,image/gif,image/bmp,image/tiff,image/webp,application/pdf" />
47</div>
48
49</template>
50
[35743]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);
[35746]65 height: calc(100vh - 3em);
66 max-height: calc(100vh - 3em);
[35743]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
[35734]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
[35743]103.image-area {
[35734]104 display: grid;
105 grid-template-columns: 1fr 1fr;
[35748]106 grid-template-rows: auto auto 1fr;
[35743]107 height: 100%;
108
[35734]109 gap: 1rem;
[35743]110 align-items: center;
111 justify-items: center;
[35734]112
113 position: relative;
114 overflow: hidden;
115
[35743]116 .text-container {
117 height: 100%;
[35734]118 width: 100%;
119 }
[35743]120}
[35734]121
[35743]122.image-display {
123 border: 1px solid #444;
124 max-width: 100%;
125 max-height: 80vh;
126 object-fit: contain;
[35734]127}
128
129.controls {
130 display: flex;
131 gap: 1em;
[35743]132 grid-column-start: span 2;
[35734]133}
[35746]134
[35748]135.ocr-progress {
136 grid-column-start: span 2;
137}
138
[35746]139.text-container {
140 overflow: scroll;
141}
[35734]142</style>
143
144<script>
145import { mapState } from "vuex";
146import EditPage from "./EditPage.vue";
[35751]147import ModalDialog from "./ModalDialog.vue"
[35734]148import OcrService, { OcrOptions } from "../js/OcrService"
[35743]149import { log } from "../js/Util";
[35734]150
151const ocrService = new OcrService();
152
153export default {
154 name: "MainPage",
155 components: {
[35751]156 EditPage,
157 ModalDialog
[35734]158 },
159 data() {
160 return {
[35746]161 imageType: null,
162 /** @type {ArrayBuffer} */
163 imageBuffer: null,
[35743]164 imageUrl: null,
165 ocrInProgress: false,
166 ocrResult: null,
[35751]167 showEditor: false,
168 modal: {
169 show: false,
170 title: "",
171 description: "",
172 buttons: []
173 }
[35734]174 }
175 },
176 computed: {
177 ...mapState({
178 translations: state => state.translations
179 })
180 },
[35746]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 },
[35734]196 methods: {
197 uploadFile() {
198 this.$refs.fileInput.click();
199 },
200 async onFilesChanged() {
201 /** @type {File[]} */
202 const files = this.$refs.fileInput.files;
[35743]203 if (files === null || files === undefined || files.length !== 1) {
[35734]204 return;
205 }
206
[35746]207 this.imageBuffer = await files[0].arrayBuffer();
208 this.imageType = files[0].type;
[35743]209 },
[35746]210
[35743]211 async doOcr() {
212 try {
213 this.ocrInProgress = true;
[35734]214
[35746]215 const arrayView = new Uint8Array(this.imageBuffer);
216 const imageBlob = new Blob([ arrayView ], { type: this.imageType });
[35743]217
218 const result = await ocrService.run([
[35734]219 {
[35743]220 image: imageBlob,
[35746]221 fileName: "file.png",
[35743]222 options: new OcrOptions(false)
[35734]223 }
[35743]224 ]);
225
226 this.ocrResult = result[0].text;
[35734]227 }
[35743]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 }
[35734]236 },
[35746]237
[35743]238 onEditorCloseRequested() {
239 this.showEditor = false;
[35734]240 },
[35746]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
[35751]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
[35743]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 = "";
[35751]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 }
[35734]286 }
287 }
288}
289</script>
Note: See TracBrowser for help on using the repository browser.