1 | <script>
|
---|
2 | import { mapState } from "vuex";
|
---|
3 | import EditPage from "./editor/EditPage.vue";
|
---|
4 | import OcrImageDisplay from "./OcrImageDisplay.vue"
|
---|
5 | import { ModalController } from "./ModalDialog.vue"
|
---|
6 | import { SnackController } from "./Snackbar.vue"
|
---|
7 | import OcrService, { OcrOptions } from "../js/OcrService"
|
---|
8 | import Util, { log } from "../js/Util";
|
---|
9 |
|
---|
10 | const ocrService = new OcrService();
|
---|
11 |
|
---|
12 | export 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>
|
---|