@operato/ocr 0.2.9 → 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/demo/index.html +699 -25
  3. package/dist/src/index.d.ts +1 -1
  4. package/dist/src/index.js +1 -1
  5. package/dist/src/index.js.map +1 -1
  6. package/dist/src/ox-camera-capturer.d.ts +14 -0
  7. package/dist/src/ox-camera-capturer.js +178 -0
  8. package/dist/src/ox-camera-capturer.js.map +1 -0
  9. package/dist/src/{ocr-helper.d.ts → ox-ocr-helper.d.ts} +8 -4
  10. package/dist/src/{ocr-helper.js → ox-ocr-helper.js} +47 -51
  11. package/dist/src/ox-ocr-helper.js.map +1 -0
  12. package/dist/src/ox-ocr-result-image.d.ts +37 -0
  13. package/dist/src/ox-ocr-result-image.js +279 -0
  14. package/dist/src/ox-ocr-result-image.js.map +1 -0
  15. package/dist/src/type.d.ts +15 -0
  16. package/dist/src/type.js +6 -0
  17. package/dist/src/type.js.map +1 -0
  18. package/dist/src/utils/file-drop-helper.d.ts +16 -0
  19. package/dist/src/utils/file-drop-helper.js +47 -0
  20. package/dist/src/utils/file-drop-helper.js.map +1 -0
  21. package/dist/src/utils/touch-to-mouse-event.d.ts +1 -0
  22. package/dist/src/utils/touch-to-mouse-event.js +34 -0
  23. package/dist/src/utils/touch-to-mouse-event.js.map +1 -0
  24. package/dist/stories/index.stories.d.ts +1 -1
  25. package/dist/stories/index.stories.js +10 -14
  26. package/dist/stories/index.stories.js.map +1 -1
  27. package/dist/test/ox-ocr-helper.test.d.ts +1 -0
  28. package/dist/test/{ocr-helper.test.js → ox-ocr-helper.test.js} +7 -7
  29. package/dist/test/ox-ocr-helper.test.js.map +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +4 -27
  32. package/src/index.ts +1 -1
  33. package/src/ox-camera-capturer.ts +189 -0
  34. package/src/{ocr-helper.ts → ox-ocr-helper.ts} +43 -49
  35. package/src/ox-ocr-result-image.ts +303 -0
  36. package/src/type.ts +13 -0
  37. package/src/utils/file-drop-helper.ts +62 -0
  38. package/src/utils/touch-to-mouse-event.ts +39 -0
  39. package/stories/index.stories.ts +29 -37
  40. package/test/{ocr-helper.test.ts → ox-ocr-helper.test.ts} +8 -7
  41. package/dist/src/ocr-helper.js.map +0 -1
  42. package/dist/test/ocr-helper.test.d.ts +0 -1
  43. package/dist/test/ocr-helper.test.js.map +0 -1
  44. package/src/camera-capturer.js +0 -184
@@ -0,0 +1,189 @@
1
+ import '@material/mwc-icon'
2
+
3
+ import { css, html, LitElement } from 'lit'
4
+ import { customElement, property, query } from 'lit/decorators.js'
5
+
6
+ import { FileDropHelper } from './utils/file-drop-helper'
7
+
8
+ @customElement('ox-camera-capturer')
9
+ export class CameraCapturer extends LitElement {
10
+ static styles = [
11
+ css`
12
+ :host {
13
+ display: flex;
14
+ flex-direction: column;
15
+ border-radius: var(--border-radius);
16
+ align-items: center;
17
+ justify-content: center;
18
+ padding: var(--padding-default, 9px);
19
+ min-height: 100px;
20
+ text-transform: capitalize;
21
+
22
+ border: var(--file-uploader-border);
23
+ background-color: var(--main-section-background-color);
24
+ font: var(--file-uploader-font) !important;
25
+ color: var(--file-uploader-color);
26
+ }
27
+ :host > mwc-icon {
28
+ color: var(--file-uploader-icon-color);
29
+ }
30
+
31
+ :host(.candrop) {
32
+ background-color: var(--file-uploader-candrop-background-color);
33
+ }
34
+
35
+ #input-file {
36
+ display: none;
37
+ }
38
+
39
+ label {
40
+ position: relative;
41
+ width: auto;
42
+ border: none;
43
+ text-transform: capitalize;
44
+
45
+ padding: var(--file-uploader-label-padding);
46
+ border-radius: var(--file-uploader-label-border-radius);
47
+ background-color: var(--file-uploader-label-background-color);
48
+ color: var(--file-uploader-label-color);
49
+ font: var(--file-uploader-label-font) !important;
50
+ }
51
+
52
+ ul {
53
+ max-width: 500px;
54
+ list-style: none;
55
+ margin: 5px 0 0 0;
56
+ padding: 0;
57
+ max-height: 46px;
58
+ overflow: auto;
59
+ background-color: rgba(var(--primary-color-rgb), 0.1);
60
+ }
61
+ li {
62
+ text-align: left;
63
+
64
+ padding: 3px 5px 2px 5px;
65
+ border-bottom: var(--file-uploader-li-border-bottom);
66
+ font: normal 14px var(--theme-font);
67
+ }
68
+ li mwc-icon {
69
+ float: right;
70
+ cursor: pointer;
71
+ margin: var(--file-uploader-li-icon-margin);
72
+ font-size: 1em;
73
+ }
74
+ li mwc-icon:hover,
75
+ li mwc-icon:active {
76
+ color: var(--file-uploader-li-icon-focus-color);
77
+ }
78
+ `
79
+ ]
80
+
81
+ @property({ type: Boolean }) multiple?: boolean
82
+ @property({ type: String }) accept?: string
83
+ @property({ type: String }) label?: string
84
+ @property({ type: Array }) _files?: File[]
85
+
86
+ render() {
87
+ var files = this._files || []
88
+
89
+ return html`
90
+ <mwc-icon>camera</mwc-icon>
91
+ <span>Capture image or drop files here!</span>
92
+ <input
93
+ id="input-file"
94
+ type="file"
95
+ accept=${this.accept}
96
+ ?multiple=${this.multiple}
97
+ hidden
98
+ capture="environment"
99
+ @change=${(e: Event) => {
100
+ const fileInput = e.currentTarget as HTMLInputElement
101
+
102
+ this._files = [...files, ...Array.from(fileInput.files as FileList)]
103
+
104
+ this.dispatchEvent(
105
+ new CustomEvent('files-change', {
106
+ bubbles: true,
107
+ composed: true,
108
+ detail: {
109
+ files: files
110
+ }
111
+ })
112
+ )
113
+
114
+ // fileInput.value = null
115
+ fileInput.files = null
116
+ }}
117
+ />
118
+ <!-- <label for="input-file">${this.label || 'capture image'}</label> -->
119
+
120
+ ${files?.length
121
+ ? html` <ul>
122
+ ${files.map(
123
+ file => html`
124
+ <li>
125
+ - ${file.name}
126
+ <mwc-icon
127
+ @click=${(e: Event) => {
128
+ files.splice(files.indexOf(file), 1)
129
+ this._files = [...files]
130
+ this.dispatchEvent(
131
+ new CustomEvent('files-change', {
132
+ bubbles: true,
133
+ composed: true,
134
+ detail: {
135
+ files: this._files
136
+ }
137
+ })
138
+ )
139
+ }}
140
+ >delete_outline</mwc-icon
141
+ >
142
+ </li>
143
+ `
144
+ )}
145
+ </ul>`
146
+ : ''}
147
+ `
148
+ }
149
+
150
+ firstUpdated() {
151
+ FileDropHelper.set(this)
152
+
153
+ this.addEventListener('file-drop', (e: Event) => {
154
+ const detail = (e as CustomEvent).detail
155
+ this._files = this.multiple ? detail : detail[0] ? [detail[0]] : []
156
+
157
+ this.dispatchEvent(
158
+ new CustomEvent('files-change', {
159
+ bubbles: true,
160
+ composed: true,
161
+ detail: {
162
+ files: this._files
163
+ }
164
+ })
165
+ )
166
+ })
167
+ }
168
+
169
+ @query('#input-file') fileInput!: HTMLInputElement
170
+
171
+ get files() {
172
+ return this._files
173
+ }
174
+
175
+ reset() {
176
+ this.fileInput.value = ''
177
+ this._files = []
178
+
179
+ this.dispatchEvent(
180
+ new CustomEvent('files-change', {
181
+ bubbles: true,
182
+ composed: true,
183
+ detail: {
184
+ files: this._files
185
+ }
186
+ })
187
+ )
188
+ }
189
+ }
@@ -1,12 +1,13 @@
1
- import './camera-capturer'
2
1
  import '@material/mwc-button'
2
+ import './ox-camera-capturer'
3
+ import './ox-ocr-result-image'
3
4
 
4
- import { LitElement, css, html } from 'lit'
5
- import { customElement, property, query } from 'lit/decorators.js'
5
+ import { css, html, LitElement } from 'lit'
6
+ import { customElement, property, query, state } from 'lit/decorators.js'
6
7
 
7
- import { gql } from 'graphql-tag'
8
+ import { NOTHING_RESULT, ScanResult } from './type'
8
9
 
9
- @customElement('ocr-helper')
10
+ @customElement('ox-ocr-helper')
10
11
  export class OcrHelper extends LitElement {
11
12
  static styles = [
12
13
  css`
@@ -23,7 +24,7 @@ export class OcrHelper extends LitElement {
23
24
 
24
25
  #images {
25
26
  flex: 1;
26
- background-color: black;
27
+ background-color: var(--main-section-background-color, #f4f7fb);
27
28
  color: white;
28
29
 
29
30
  display: flex;
@@ -37,46 +38,41 @@ export class OcrHelper extends LitElement {
37
38
 
38
39
  [i] {
39
40
  min-width: 50px;
40
- background-color: black;
41
+ background-color: var(--secondary-color, #394e64);
42
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
43
+ padding: var(--padding-narrow, 4px);
41
44
  text-align: center;
45
+ height: 1.4em;
42
46
  }
43
47
 
44
48
  [i][active] {
45
- background-color: red;
49
+ background-color: var(--primary-color, #22a6a7);
50
+ font-weight: bold;
46
51
  }
47
52
 
48
- [image] {
53
+ ox-ocr-result-image {
49
54
  flex: 1;
50
-
51
- display: flex;
52
- flex-direction: column;
53
- justify-content: center;
54
- }
55
-
56
- img {
57
- style=width: 100%;
58
55
  }
59
56
  `
60
57
  ]
61
58
 
62
- @property({ type: Array }) _files: any
63
- @property({ type: Number }) index: number = 0
59
+ @property({ type: Array }) result: ScanResult[] = []
60
+
61
+ @state() _files: any
62
+ @state() _index: number = 0
64
63
 
65
64
  @query('#capturer') fileUploader!: any
66
- @query('#result') result!: any
65
+ @query('#copied') copied!: HTMLElement
67
66
 
68
67
  render() {
69
68
  const files = this._files || []
70
- const index = this.index || 0
69
+ const index = this._index || 0
71
70
  const file = files[index]
72
- const image = file && URL.createObjectURL(file)
73
- if (image) {
74
- requestAnimationFrame(() => URL.revokeObjectURL(image))
75
- }
71
+ const result = this.result?.[index] || NOTHING_RESULT
76
72
 
77
73
  return html`
78
74
  <div attachment-wrap>
79
- <camera-capturer
75
+ <ox-camera-capturer
80
76
  name="scanner"
81
77
  id="capturer"
82
78
  label="Capture Images"
@@ -85,10 +81,10 @@ export class OcrHelper extends LitElement {
85
81
  ?hidden=${false}
86
82
  custom-input
87
83
  @files-change=${(e: Event) => this.fileschange(e)}
88
- ></camera-capturer>
84
+ ></ox-camera-capturer>
89
85
  </div>
90
86
  <div buttons>
91
- <mwc-button @click=${(e: Event) => this.upload()}>submit</mwc-button>
87
+ <mwc-button @click=${(e: Event) => this.submit()}>submit</mwc-button>
92
88
  <mwc-button @click=${(e: Event) => this.reset()}>reset</mwc-button>
93
89
  </div>
94
90
  <div id="images">
@@ -98,15 +94,20 @@ export class OcrHelper extends LitElement {
98
94
  const item = (e.target as HTMLElement)?.closest('[i]')
99
95
  const children = (e.currentTarget as HTMLElement)?.querySelectorAll('[i]')
100
96
  if (item) {
101
- this.index = Array.from(children).indexOf(item)
97
+ this._index = Array.from(children).indexOf(item)
102
98
  }
103
99
  }}
104
100
  >
105
101
  ${files.map((file: any, idx: number) => html`<div i ?active=${idx === index}>${idx}</div>`)}
106
102
  </div>
107
- <div image>${file ? html` <img src=${URL.createObjectURL(file)} /> ` : html``}</div>
103
+ <ox-ocr-result-image
104
+ .file=${file}
105
+ @selected=${(e: CustomEvent) => this.onSelected(e)}
106
+ .result=${result}
107
+ ></ox-ocr-result-image>
108
108
  </div>
109
- <div id="result"></div>
109
+
110
+ <div id="copied"></div>
110
111
  `
111
112
  }
112
113
 
@@ -116,25 +117,18 @@ export class OcrHelper extends LitElement {
116
117
 
117
118
  reset() {
118
119
  this.fileUploader.reset()
119
- this.result.textContent = ''
120
+ this.copied.textContent = ''
121
+ }
122
+
123
+ onSelected(e: CustomEvent) {
124
+ navigator.clipboard.writeText(e.detail)
120
125
  }
121
126
 
122
- async upload() {
123
- // const response = await client.query({
124
- // query: gql`
125
- // query ocrRequest($images: [Upload!]!) {
126
- // ocrRequest(images: $images)
127
- // }
128
- // `,
129
- // variables: {
130
- // images: this.fileUploader?.files
131
- // },
132
- // context: { hasUpload: true }
133
- // })
134
- // if (!response.errors) {
135
- // this.result.textContent = response.data.ocrRequest
136
- // } else {
137
- // this.result.textContent = 'error'
138
- // }
127
+ async submit() {
128
+ this.dispatchEvent(
129
+ new CustomEvent('attached', {
130
+ detail: this.fileUploader?.files
131
+ })
132
+ )
139
133
  }
140
134
  }
@@ -0,0 +1,303 @@
1
+ import { css, html, LitElement, PropertyValues } from 'lit'
2
+ import { customElement, property, query, state } from 'lit/decorators.js'
3
+
4
+ import { NOTHING_RESULT, ScanResult } from './type'
5
+ import { touch2mouse } from './utils/touch-to-mouse-event'
6
+
7
+ @customElement('ox-ocr-result-image')
8
+ export class OcrResultImage extends LitElement {
9
+ static styles = [
10
+ css`
11
+ :host {
12
+ display: block;
13
+ position: relative;
14
+ }
15
+
16
+ img {
17
+ display: block;
18
+ flex: 1;
19
+ border: 0;
20
+ padding: 0;
21
+ margin: 0;
22
+ max-width: 100%;
23
+ max-height: 100%;
24
+ }
25
+
26
+ [overlay] {
27
+ position: absolute;
28
+ top: 0;
29
+ left: 0;
30
+ width: 100%;
31
+ height: 100%;
32
+
33
+ border: 0;
34
+ padding: 0;
35
+ margin: 0;
36
+ opacity: 50%;
37
+ }
38
+
39
+ [notice] {
40
+ position: absolute;
41
+ bottom: 0;
42
+ background-color: var(--primary-color, #38a25b);
43
+ padding: var(--padding-default, 9px) var(--padding-wide, 15px);
44
+ opacity: 0.95;
45
+
46
+ overflow-wrap: anywhere;
47
+
48
+ color: #fff;
49
+ font-size: 1.5em;
50
+ }
51
+ `
52
+ ]
53
+
54
+ @property({ type: Array }) file: any
55
+ @property({ type: Array }) result: ScanResult = NOTHING_RESULT
56
+
57
+ @state() _ratio: { x: number; y: number } = { x: 1, y: 1 }
58
+ @state() _image!: string
59
+ @state() _resizeObserver!: ResizeObserver
60
+ @state() _trails: Array<Array<{ x: number; y: number }>> = []
61
+ @state() _tracking: boolean = false
62
+ @state() _selections: Array<number> = []
63
+ @state() _text: string = ''
64
+
65
+ @query('img') _img!: HTMLImageElement
66
+ @query('[overlay]') _overlay!: HTMLCanvasElement
67
+
68
+ render() {
69
+ return html`
70
+ <img src=${this._image} @load=${() => this.calculateRatio()} />
71
+ <canvas overlay></canvas>
72
+
73
+ ${this._text ? html` <div notice>${this._text} copied!</div> ` : html``}
74
+ `
75
+ }
76
+
77
+ renderOverlay() {
78
+ const { boundaries } = this.result
79
+
80
+ const context = this._overlay.getContext('2d')
81
+ if (!context) {
82
+ return
83
+ }
84
+
85
+ context.save()
86
+
87
+ context.clearRect(0, 0, this._overlay.width, this._overlay.height)
88
+
89
+ context.lineWidth = 5
90
+ context.strokeStyle = 'red'
91
+ context.fillStyle = 'red'
92
+ context.lineJoin = 'round'
93
+ context.lineCap = 'round'
94
+
95
+ this._image &&
96
+ boundaries
97
+ .map((bounds): Array<{ x: number; y: number }> => {
98
+ return bounds.map(bound => {
99
+ return {
100
+ x: bound.x, //* this._ratio.x,
101
+ y: bound.y // * this._ratio.y
102
+ }
103
+ })
104
+ })
105
+ .forEach((bounds, idx) => {
106
+ context.beginPath()
107
+ context.moveTo(bounds[0].x, bounds[0].y)
108
+ context.lineTo(bounds[1].x, bounds[1].y)
109
+ context.lineTo(bounds[2].x, bounds[2].y)
110
+ context.lineTo(bounds[3].x, bounds[3].y)
111
+ context.closePath()
112
+
113
+ if (this._selections.includes(idx)) {
114
+ context.fill()
115
+ }
116
+ context.stroke()
117
+ })
118
+
119
+ /* draw trails */
120
+ if (this._trails && this._trails.length > 0) {
121
+ context.lineWidth = 10
122
+ context.strokeStyle = 'blue'
123
+
124
+ context.beginPath()
125
+
126
+ this._trails.forEach(trail => {
127
+ if (trail && trail.length > 0) {
128
+ const { x, y } = trail[0]
129
+ context.moveTo(x, y)
130
+
131
+ trail.slice(1).forEach(point => {
132
+ const { x, y } = point
133
+ context.lineTo(x, y)
134
+ })
135
+ }
136
+ })
137
+
138
+ context.stroke()
139
+ }
140
+
141
+ context.restore()
142
+ }
143
+
144
+ async calculateRatio() {
145
+ const { naturalWidth, naturalHeight, offsetWidth, offsetHeight } = this._img
146
+ const { offsetWidth: overlayWidth, offsetHeight: overlayHeight } = this._overlay
147
+
148
+ if (!naturalWidth || !naturalHeight) {
149
+ return
150
+ }
151
+
152
+ this._ratio = {
153
+ x: offsetWidth / naturalWidth,
154
+ y: offsetHeight / naturalHeight
155
+ }
156
+
157
+ this._overlay.setAttribute('width', String(overlayWidth / this._ratio.x))
158
+ this._overlay.setAttribute('height', String(overlayHeight / this._ratio.y))
159
+ }
160
+
161
+ async connectedCallback() {
162
+ await super.connectedCallback()
163
+
164
+ this._resizeObserver = new ResizeObserver(entries => {
165
+ this.calculateRatio()
166
+ this.renderOverlay()
167
+ })
168
+
169
+ await this.updateComplete
170
+ this._resizeObserver.observe(this._overlay)
171
+
172
+ this.calculateRatio()
173
+
174
+ this.addEventListener('mousedown', this.onTrailStart.bind(this) as EventListener)
175
+ this.addEventListener('mouseup', this.onTrailEnd.bind(this) as EventListener)
176
+ this.addEventListener('mousemove', this.onTrail.bind(this) as EventListener)
177
+ this.addEventListener('touchstart', touch2mouse as EventListener)
178
+ this.addEventListener('touchend', touch2mouse as EventListener)
179
+ this.addEventListener('touchmove', touch2mouse as EventListener)
180
+ }
181
+
182
+ disconnectedCallback() {
183
+ super.disconnectedCallback()
184
+
185
+ this._resizeObserver.disconnect()
186
+ }
187
+
188
+ initialize() {
189
+ this._selections = []
190
+ this._trails = []
191
+ this._text = ''
192
+ }
193
+
194
+ async updated(changes: PropertyValues<this>) {
195
+ if (changes.has('file') || changes.has('result')) {
196
+ this.initialize()
197
+ }
198
+
199
+ if (changes.has('file')) {
200
+ this._image = this.file && URL.createObjectURL(this.file)
201
+ if (this._image) {
202
+ requestAnimationFrame(() => {
203
+ URL.revokeObjectURL(this._image)
204
+ })
205
+
206
+ await this.updateComplete
207
+ this.calculateRatio()
208
+ }
209
+ }
210
+
211
+ this.renderOverlay()
212
+ }
213
+
214
+ findIndex(x: number, y: number): number {
215
+ const { boundaries } = this.result
216
+
217
+ return boundaries.findIndex(path => {
218
+ var result = false
219
+
220
+ path.forEach((p, idx) => {
221
+ let j = (idx + path.length + 1) % path.length
222
+
223
+ let x1 = p.x
224
+ let y1 = p.y
225
+ let x2 = path[j].x
226
+ let y2 = path[j].y
227
+
228
+ if (y1 > y != y2 > y && x < ((x2 - x1) * (y - y1)) / (y2 - y1) + x1) {
229
+ result = !result
230
+ }
231
+ })
232
+
233
+ return result
234
+ })
235
+ }
236
+
237
+ updateTrails(point: { x: number; y: number }, started: boolean, attached?: boolean) {
238
+ if (started && !attached) {
239
+ this.initialize()
240
+ }
241
+
242
+ if (started) {
243
+ this._trails.push([])
244
+ }
245
+
246
+ var trail = this._trails[this._trails.length - 1]
247
+ if (!trail) {
248
+ return
249
+ }
250
+
251
+ trail.push(point)
252
+
253
+ this._trails.flat().forEach(({ x, y }) => {
254
+ const idx = this.findIndex(x, y)
255
+ if (idx === -1) {
256
+ return
257
+ }
258
+
259
+ if (!this._selections.includes(idx)) {
260
+ this._selections.push(idx)
261
+ }
262
+ })
263
+
264
+ this.requestUpdate()
265
+ }
266
+
267
+ onTrailStart(e: MouseEvent) {
268
+ const { offsetX: x, offsetY: y, button, shiftKey } = e
269
+
270
+ if (button !== 0) {
271
+ return
272
+ }
273
+
274
+ this._tracking = true
275
+ this.updateTrails({ x: x / this._ratio.x, y: y / this._ratio.y }, true, shiftKey)
276
+ }
277
+
278
+ onTrailEnd(e: MouseEvent) {
279
+ if (!this._tracking) {
280
+ return
281
+ }
282
+
283
+ this._text = this._selections.map(idx => this.result.texts[idx]).join('')
284
+
285
+ this.dispatchEvent(
286
+ new CustomEvent('selected', {
287
+ detail: this._text
288
+ })
289
+ )
290
+
291
+ this._tracking = false
292
+ }
293
+
294
+ onTrail(e: MouseEvent) {
295
+ const { offsetX: x, offsetY: y } = e
296
+
297
+ if (!this._tracking) {
298
+ return
299
+ }
300
+
301
+ this.updateTrails({ x: x / this._ratio.x, y: y / this._ratio.y }, false)
302
+ }
303
+ }
package/src/type.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type ScanResult = {
2
+ key_values: {
3
+ [key: string]: Array<string>
4
+ }
5
+ boundaries: Array<Array<{ x: number; y: number }>>
6
+ texts: Array<string>
7
+ }
8
+
9
+ export const NOTHING_RESULT = {
10
+ boundaries: [],
11
+ key_values: {},
12
+ texts: []
13
+ }