Changeset 35743


Ignore:
Timestamp:
2021-11-24T10:57:13+13:00 (2 years ago)
Author:
cstephen
Message:

Rework for single image content.
Implement rotation and inversion editing.

Location:
main/trunk/model-interfaces-dev/atea/ocr
Files:
1 added
2 deleted
5 edited

Legend:

Unmodified
Added
Removed
  • main/trunk/model-interfaces-dev/atea/ocr

    • Property svn:ignore
      •  

        old new  
        11dist
         2node_modules
  • main/trunk/model-interfaces-dev/atea/ocr/src/App.vue

    r35734 r35743  
    11<template>
    22<div class="root">
    3     <div class="app-bar theme-primary">
     3    <!-- <div class="app-bar theme-primary">
    44        <div class="app-bar-content">
    55            <span class="heading1">{{ translations.get("Title") }}</span>
    66        </div>
    7     </div>
     7    </div> -->
    88
    9     <div class="content">
    10         <main-page />
    11     </div>
     9    <main-page class="content" />
    1210</div>
    1311</template>
  • main/trunk/model-interfaces-dev/atea/ocr/src/components/EditPage.vue

    r35734 r35743  
    11<template>
    2 <div class="root">
     2<div class="edit-page-root">
    33    <div class="image-panel">
    4         <table v-if="rotateMode" class="rotate-guide">
    5             <tr><hr /></tr>
    6             <tr><hr /></tr>
    7             <tr><hr /></tr>
    8             <tr><hr /></tr>
    9             <tr><hr /></tr>
    10             <tr><hr /></tr>
    11             <tr><hr /></tr>
    12             <tr><hr /></tr>
    13             <tr><hr /></tr>
    14             <tr><hr /></tr>
    15             <tr><hr /></tr>
    16             <tr><hr /></tr>
    17         </table>
    18 
    19         <!-- <img :src="imageUrl" class="image" :style="{ filter: filterString, transform: transformString }" /> -->
    20         <div class="image-container">
    21             <img v-if="rotateMode" src="images/rotate_arrow_64.png" class="rotate-handle rotate-tl" />
    22             <img v-if="rotateMode" src="images/rotate_arrow_64.png" class="rotate-handle rotate-tr" />
    23             <img v-if="rotateMode" src="images/rotate_arrow_64.png" class="rotate-handle rotate-bl" />
    24             <img v-if="rotateMode" src="images/rotate_arrow_64.png" class="rotate-handle rotate-br" />
    25 
    26             <img :src="imageUrl" class="image" :style="{ filter: filterString, transform: transformString }" />
     4        <div class="image-container" :style="{ transform: transformString }">
     5            <img v-if="rotateMode" src="images/rotate-arrow.svg" class="rotate-handle rotate-tl" @mousedown="allowRotation = true" draggable="false" />
     6            <img v-if="rotateMode" src="images/rotate-arrow.svg" class="rotate-handle rotate-tr" @mousedown="allowRotation = true" draggable="false" />
     7            <img v-if="rotateMode" src="images/rotate-arrow.svg" class="rotate-handle rotate-bl" @mousedown="allowRotation = true" draggable="false" />
     8            <img v-if="rotateMode" src="images/rotate-arrow.svg" class="rotate-handle rotate-br" @mousedown="allowRotation = true" draggable="false" />
     9
     10            <table v-if="rotateMode" class="rotate-guide" :style="{ transform: transformStringInverted }">
     11                <tr><td><hr /></td></tr>
     12                <tr><td><hr /></td></tr>
     13                <tr><td><hr /></td></tr>
     14                <tr><td><hr /></td></tr>
     15                <tr><td><hr /></td></tr>
     16                <tr><td><hr /></td></tr>
     17                <tr><td><hr /></td></tr>
     18                <tr><td><hr /></td></tr>
     19                <tr><td><hr /></td></tr>
     20                <tr><td><hr /></td></tr>
     21                <tr><td><hr /></td></tr>
     22                <tr><td><hr /></td></tr>
     23            </table>
     24
     25            <img ref="image" :src="imageUrl" class="image" :style="{ filter: filterString }" />
    2726        </div>
    2827    </div>
     
    4544
    4645<style lang="scss" scoped>
    47 .root {
     46.edit-page-root {
    4847    display: grid;
    4948    grid-template-columns: 1fr auto;
    50     grid-template-rows: 100%;
    5149    align-items: center;
    5250    justify-items: center;
     
    6563
    6664.image {
    67     max-width: 100%;
    68     height: auto;
     65    max-width: 70vw;
     66    max-height: 70vh;
    6967}
    7068
    7169.image-container {
    72     max-width: 70%;
    73     max-height: 70%;
    7470    position: relative;
    7571    margin: auto;
    76     padding: 1em;
     72    padding: 1.5em;
     73    user-select: none;
    7774
    7875    .rotate-handle {
    7976        height: 2em;
    8077        position: absolute;
     78        z-index: 5;
    8179
    8280        // Generated with https://codepen.io/sosuke/pen/Pjoqqp using var(--primary-bg-color)
    83         filter: invert(16%) sepia(100%) saturate(3091%) hue-rotate(347deg) brightness(75%) contrast(90%);
     81        // filter: invert(16%) sepia(100%) saturate(3091%) hue-rotate(347deg) brightness(75%) contrast(90%);
    8482    }
    8583
     
    116114    z-index: 3;
    117115
    118     tr hr {
    119         background-color: rgba(var(--bg-color-raw), 0.5);
     116    tr {
     117        td {
     118            vertical-align: center;
     119        }
     120
     121        hr {
     122            background-color: rgba(var(--bg-color-raw), 0.5);
     123            height: 2px;
     124        }
    120125    }
    121126}
     
    137142<script>
    138143import { mapState } from "vuex";
     144import Jimp from "jimp/es";
    139145import ToggleButton from "./ToggleButton.vue"
    140146
     
    151157            imageUrl: URL.createObjectURL(this.image),
    152158            invert: false,
    153             rotateMode: false
     159            rotation: 0,
     160            rotateMode: false,
     161            allowRotation: false
    154162        }
    155163    },
     
    167175            },
    168176            transformString() {
    169                 return `rotate(${this.rotation}deg)`
     177                return `rotate(${this.rotation}deg)`;
     178            },
     179            transformStringInverted() {
     180                return `rotate(-${this.rotation}deg)`;
    170181            }
    171182        })
    172183    },
    173184    methods: {
     185        async getImageBlobAsync() {
     186            const buffer = Buffer.from(this.image.arrayBuffer());
     187            const that = this;
     188
     189            const modifiedBuffer = await Jimp.read(buffer)
     190                .then(async image => {
     191                    if (that.invert) {
     192                        image.invert();
     193                    }
     194
     195                    // if (that.rotation !== 0) {
     196                    //     image.rotate(that.rotation);
     197                    // }
     198
     199                    return await image.getBufferAsync(Jimp.MIME_PNG);
     200                });
     201
     202            return new Blob([ modifiedBuffer ], { type: Jimp.MIME_PNG });
     203        },
     204
     205        onRotateHandleMouseDown(point) {
     206            console.log(point);
     207            this.rotatePoint = point;
     208        },
     209        /**
     210         * @param {MouseEvent} e The mouse movement event.
     211         */
     212        onDocumentMouseMove(e) {
     213            if (!this.allowRotation) {
     214                return;
     215            }
     216
     217            const imageRectangle = this.$refs.image.getBoundingClientRect();
     218            const imageCenterX = (imageRectangle.width / 2) + imageRectangle.left;
     219            const imageCenterY = (imageRectangle.height / 2) + imageRectangle.top;
     220
     221            if (e.clientX > imageCenterX) {
     222                this.rotation += e.movementY / 8;
     223            }
     224            else {
     225                this.rotation -= e.movementY / 8;
     226            }
     227
     228            if (e.clientY > imageCenterY) {
     229                this.rotation -= e.movementX / 8;
     230            }
     231            else {
     232                this.rotation += e.movementX / 8;
     233            }
     234
     235            if (this.rotation < 0) {
     236                this.rotation = 360 + this.rotation;
     237            }
     238            else if (this.rotation > 360) {
     239                this.rotation -= 360;
     240            }
     241        },
     242        onDocumentMouseUp() {
     243            this.allowRotation = false;
     244        }
     245    },
     246    beforeMount() {
     247        document.addEventListener("mousemove", this.onDocumentMouseMove);
     248        document.addEventListener("mouseup", this.onDocumentMouseUp);
     249    },
     250    beforeUnmount() {
     251        document.removeEventListener("mousemove", this.onDocumentMouseMove);
     252        document.removeEventListener("mouseup", this.onDocumentMouseUp);
    174253    }
    175254}
  • main/trunk/model-interfaces-dev/atea/ocr/src/components/MainPage.vue

    r35734 r35743  
    11<template>
    2 <div class="root">
    3     <edit-page v-if="imageToEdit != null" class="image-editor" :image="imageToEdit" @closeRequested="onEditorCloseRequested" />
    4 
    5     <button class="btn-primary spacing-bottom" @click="uploadFile">
    6         <span class="material-icons">upload</span>
    7         <span>{{ translations.get("Main_UploadImages") }}</span>
    8     </button>
    9 
    10     <div v-for="[id, imageInfo] in images" :key="id" class="image-list card"
    11         :style="{ 'max-height': imageInfo.isExpanded ? 'none' : '12em' }">
    12         <image-display :imageBuffer="imageInfo.buffer" :type="imageInfo.image.type" :ref="id" />
    13 
    14         <div class="controls">
    15             <button class="btn-primary" @click="doOcr(id)" :disabled="imageInfo.ocr">
    16                 <span class="material-icons">play_arrow</span>
    17                 <span>{{ translations.get("Main_PerformOCR") }}</span>
    18             </button>
    19 
    20             <!-- <div v-if="imageInfo.ocr" class="progress-bar-container">
    21                 <div class="progress-bar-value progress-bar-indeterminate" />
    22             </div> -->
    23             <button class="btn-primary" @click="imageToEdit = imageInfo.image">Edit</button>
     2<div class="main-page-root">
     3    <edit-page v-if="showEditor" class="image-editor" :image="image" @closeRequested="onEditorCloseRequested" />
     4
     5    <div class="paper root-container" :class="{ 'root-container-image-state': image !== null }">
     6        <div v-if="image === null" class="upload-area" @click="uploadFile">
     7            <span class="heading1">{{ translations.get("Title") }}</span>
     8            <span class="material-icons mdi-xl">upload_file</span>
     9            <span>Upload an image/PDF</span>
     10        </div>
     11
     12        <div v-if="image !== null" class="image-area">
     13            <div class="controls">
     14                <button class="btn-primary" @click="doOcr" :disabled="ocrInProgress">
     15                    <span class="material-icons">play_arrow</span>
     16                    <span>{{ translations.get("Main_PerformOCR") }}</span>
     17                </button>
     18
     19                <button class="btn-primary" @click="showEditor = true">
     20                    <span class="material-icons">edit</span>
     21                    <span>Edit Image</span>
     22                </button>
     23
     24                <button class="btn-primary" @click="reset" :disabled="ocrInProgress">
     25                    <span class="material-icons">restart_alt</span>
     26                    <span>New</span>
     27                </button>
     28
     29                <div v-if="ocrInProgress" class="progress-bar-container">
     30                    <div class="progress-bar-value progress-bar-indeterminate" />
     31                </div>
     32            </div>
     33
     34            <img class="image-display" :src="imageUrl" />
    2435
    2536            <div class="text-container">
    26                 <pre>{{ imageInfo.text }}</pre>
     37                <pre>{{ ocrResult }}</pre>
    2738            </div>
    2839        </div>
    29 
    30         <div class="expander-spacer" />
    31         <div v-if="!imageInfo.isExpanded" class="washout" />
    32 
    33         <button class="btn-primary theme-flat expander" @click="imageInfo.isExpanded = !imageInfo.isExpanded">
    34             v Expand
    35         </button>
    3640    </div>
    3741
    38     <input ref="fileInput" type="file" @input="onFilesChanged" class="hidden" multiple
     42    <input ref="fileInput" type="file" @input="onFilesChanged" class="hidden"
    3943        accept="image/png,image/jpeg,image/gif,image/bmp,image/tiff,image/webp,application/pdf" />
    4044</div>
     
    4246</template>
    4347
    44 <style scoped lang="scss">
     48<style lang="scss" scoped>
     49.main-page-root {
     50    display: flex;
     51    align-items: center;
     52    justify-content: center;
     53}
     54
     55.root-container {
     56    padding: 1em;
     57    transition-duration: var(--transition-duration);
     58}
     59
     60.root-container-image-state {
     61    width: calc(100% - 3em);
     62    height: calc(100% - 3em);
     63}
     64
     65.upload-area {
     66    display: grid;
     67    grid-template-columns: auto auto;
     68    grid-template-rows: auto 1fr;
     69    align-items: center;
     70    justify-items: center;
     71
     72    padding: 2em;
     73    gap: 1em;
     74
     75    font-size: 2rem;
     76    cursor: pointer;
     77    border: 3px dashed var(--bg-color);
     78    border-radius: var(--border-radius);
     79
     80    .heading1 {
     81        grid-column-start: span 2;
     82        color: #666;
     83    }
     84}
     85
    4586.hidden {
    4687    display: none;
    47 }
    48 
    49 .spacing-bottom {
    50     margin-bottom: 1em;
    5188}
    5289
     
    6097}
    6198
    62 .image-list {
     99.image-area {
    63100    display: grid;
    64101    grid-template-columns: 1fr 1fr;
     102    grid-template-rows: auto 1fr;
     103    height: 100%;
     104
    65105    gap: 1rem;
     106    align-items: center;
     107    justify-items: center;
    66108
    67109    position: relative;
    68110    overflow: hidden;
    69111
    70     margin-bottom: 1rem;
    71 
    72     .expander {
    73         position: absolute;
    74         bottom: 0;
     112    .text-container {
     113        height: 100%;
    75114        width: 100%;
    76         z-index: 2;
    77115    }
    78 
    79     .expander-spacer {
    80         height: 1em;
    81     }
    82 
    83     .washout {
    84         position: absolute;
    85         bottom: 0;
    86         height: 60%;
    87         width: 100%;
    88         background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgb(247, 247, 247) 95%);
    89     }
     116}
     117
     118.image-display {
     119    border: 1px solid #444;
     120    max-width: 100%;
     121    max-height: 80vh;
     122    object-fit: contain;
    90123}
    91124
    92125.controls {
    93126    display: flex;
    94     flex-direction: column;
    95127    gap: 1em;
     128    grid-column-start: span 2;
    96129}
    97130</style>
     
    100133import { mapState } from "vuex";
    101134import EditPage from "./EditPage.vue";
    102 import ImageDisplay from "./ImageDisplay.vue"
    103135import OcrService, { OcrOptions } from "../js/OcrService"
    104 import Util from "../js/Util";
     136import { log } from "../js/Util";
    105137
    106138const ocrService = new OcrService();
     
    109141    name: "MainPage",
    110142    components: {
    111         ImageDisplay,
    112143        EditPage
    113144    },
    114145    data() {
    115146        return {
    116             /** @type {Map<String, {image: File, buffer: ArrayBuffer, text: String, ocr: Boolean, isExpanded: Boolean}>} */
    117             images: new Map(),
    118             imageToEdit: null
     147            /** @type {File} */
     148            image: null,
     149            imageUrl: null,
     150            ocrInProgress: false,
     151            ocrResult: null,
     152            showEditor: false
    119153        }
    120154    },
     
    131165            /** @type {File[]} */
    132166            const files = this.$refs.fileInput.files;
    133             if (files === null || files === undefined || files.length < 1) {
     167            if (files === null || files === undefined || files.length !== 1) {
    134168                return;
    135169            }
    136170
    137             for (const file of files) {
    138                 const buffer = await file.arrayBuffer();
    139 
    140                 this.images.set(
    141                     Util.generateUuid(),
     171            this.image = files[0];
     172            this.imageUrl = URL.createObjectURL(this.image);
     173        },
     174        async doOcr() {
     175            try {
     176                this.ocrInProgress = true;
     177
     178                const imageBlob = await this.$refs.imageEditor.getImageBlobAsync();
     179
     180                const result = await ocrService.run([
    142181                    {
    143                         image: file,
    144                         buffer: buffer
     182                        image: imageBlob,
     183                        options: new OcrOptions(false)
    145184                    }
    146                 )
    147             }
    148         },
    149         async doOcr(id) {
    150             // TODO: Catch and log error
    151 
    152             const imageInfo = this.images.get(id);
    153             imageInfo.ocr = true;
    154 
    155             const imageBlob = await this.$refs[id].getImageBlobAsync();
    156 
    157             const result = await ocrService.run([
    158                 {
    159                     image: imageBlob,
    160                     options: new OcrOptions(false)
    161                 }
    162             ]);
    163 
    164             imageInfo.ocr = false;
    165             imageInfo.text = result[0].text;
     185                ]);
     186
     187                this.ocrResult = result[0].text;
     188            }
     189            catch (ex) {
     190                // TODO: Display error
     191                log("Failed to perform OCR", "error");
     192                log(ex, "error");
     193            }
     194            finally {
     195                this.ocrInProgress = false;
     196            }
    166197        },
    167198        onEditorCloseRequested() {
    168             this.imageToEdit = null;
     199            this.showEditor = false;
     200        },
     201        reset() {
     202            this.image = null;
     203            URL.revokeObjectURL(this.imageUrl);
     204            this.imageUrl = null;
     205            this.ocrInProgress = false;
     206            this.ocrResult = null;
     207            this.showEditor = false;
     208            this.$refs.fileInput.value = "";
    169209        }
    170210    }
  • main/trunk/model-interfaces-dev/atea/ocr/src/main.js

    r35734 r35743  
    11import { createApp } from "vue";
    22import { createStore } from "vuex"
    3 import Util from "./js/Util"
    43import App from "./App.vue";
    54import ToggleButton from "./components/ToggleButton.vue"
     
    98        return {
    109            /** @type {Map<String, String>} */
    11             translations: new Map(),
    12             macronisedFileInfos: [],
    13             directInput: null,
    14             directOutput: []
     10            translations: new Map()
    1511        }
    1612    },
     
    1814        setTranslations(state, translations) {
    1915            state.translations = translations;
    20         },
    21         pushFileInfo(state, fileInfo) {
    22             state.macronisedFileInfos.push({
    23                 id: Util.generateUuid(),
    24                 ...fileInfo
    25             });
    26         },
    27         setDirectInput(state, input) {
    28             state.directInput = input;
    29         },
    30         setDirectOutput(state, output) {
    31             state.directOutput = output;
    3216        }
    3317    }
     
    4832/* eslint-disable no-undef */
    4933if (typeof gs !== "undefined" && gs.text && gs.text.atea && gs.text.atea.ocr) {
    50     for (const key in gs.text.atea) {
    51         translations.set(key, gs.text.atea[key]);
     34    for (const key in gs.text.atea.ocr) {
     35        translations.set(key, gs.text.atea.ocr[key]);
    5236    }
    5337
Note: See TracChangeset for help on using the changeset viewer.