Skip to content

Commit 05c6685

Browse files
committed
initial commit
0 parents  commit 05c6685

7 files changed

Lines changed: 403 additions & 0 deletions

File tree

.github/workflows/publish.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Publish to NPM
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout Source
13+
uses: actions/checkout@v4
14+
15+
- name: Setup Node.js
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
registry-url: 'https://registry.npmjs.org'
20+
21+
- name: Install Dependencies
22+
run: npm ci
23+
24+
- name: Build Library
25+
run: npm run build
26+
27+
- name: Publish to NPM
28+
run: npm publish
29+
env:
30+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
dist/
3+
package-lock.json
4+
.DS_Store

LICENSE

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Public-Source Corporate Royalty License (PSCRL)
2+
3+
Copyright (c) 2026 Alif Nurhidayat (alifnurhidayatwork@gmail.com)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, person, educational,
8+
research, and open-source public use, subject to the following conditions:
9+
10+
1. NON-COMMERCIAL & PUBLIC USE
11+
The Software is completely free to use, modify, and distribute for individuals,
12+
educational institutions, personal research, testing, and non-commercial open-source
13+
projects.
14+
15+
2. CORPORATE & COMMERCIAL BUSINESS USE
16+
Any use of the Software by for-profit companies, corporations, commercial applications,
17+
SaaS (Software as a Service) platforms, or for any revenue-generating activity
18+
requires a commercial licensing agreement.
19+
20+
By using this Software for commercial or corporate purposes, you agree to:
21+
a. Pay a royalty fee of 1% (one percent) of the gross monthly revenue generated by
22+
any product, service, or platform that incorporates or uses this Software.
23+
b. Startups or businesses with gross annual revenues of less than $10,000 USD (or equivalent)
24+
are exempt from royalties until they exceed this threshold.
25+
c. Provide quarterly reporting of gross revenues generated to the copyright holder.
26+
27+
3. CONTACT & CUSTOM LICENSING
28+
For royalty payments, reporting, custom enterprise flat-rate licenses, or general inquiries,
29+
contact the copyright holder directly at:
30+
alifnurhidayatwork@gmail.com
31+
32+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38+
SOFTWARE.

README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# @craftthingy-digital-innovation/cty-ocr-queue
2+
3+
Bilingual documentation: [Bahasa Indonesia](#bahasa-indonesia) | [English](#english)
4+
5+
---
6+
7+
## Bahasa Indonesia
8+
9+
Library JavaScript modular isomorphic (Universal) untuk mengelola antrean pemrosesan foto OCR (Optical Character Recognition) secara berurutan (sekuensial) baik di browser maupun di server Node.js demi menghemat pemakaian memori RAM & CPU komputer client/server.
10+
11+
### Fitur Utama
12+
- ⏱️ **Sequential Processing:** Mengantrekan pemrosesan gambar secara teratur (satu per satu) untuk mencegah tabrakan memory akibat pemanggilan library AI paralel.
13+
- ⚙️ **Queue Lifecycle Status:** Melacak status pengerjaan setiap item (`pending`, `processing`, `ready`, `error`).
14+
- 🌐 **Isomorphic & Environment Agnostic:** Bekerja mulus di Browser (menggunakan DOM Image loader) maupun di Node.js (menggunakan file paths, buffers, atau stream).
15+
16+
### Instalasi
17+
```bash
18+
npm install @craftthingy-digital-innovation/cty-ocr-queue
19+
```
20+
21+
### Cara Penggunaan
22+
```javascript
23+
import { OcrQueueManager } from '@craftthingy-digital-innovation/cty-ocr-queue';
24+
25+
const ocrQueue = new OcrQueueManager({
26+
getOcrClient: async () => {
27+
// Kembalikan instansi PaddleOCRClient / Tesseract Anda
28+
return myOcrClientInstance;
29+
},
30+
onQueueUpdate: (queue) => {
31+
console.log("Antrean Terkini:", queue);
32+
},
33+
onItemReady: (item, index) => {
34+
console.log(`Foto ${item.filename} berhasil di-OCR!`, item.results);
35+
}
36+
});
37+
38+
// Menambahkan gambar baru ke antrean
39+
ocrQueue.enqueue({
40+
id: 'image-uuid-abc',
41+
filename: 'paspor_budi.jpg',
42+
url: 'data:image/jpeg;base64,...' // browser URL atau Node.js Buffer
43+
});
44+
```
45+
46+
---
47+
48+
## English
49+
50+
A modular isomorphic (Universal) JavaScript library to manage background OCR (Optical Character Recognition) image queues sequentially across both Browser and Node.js environments.
51+
52+
### Key Features
53+
- ⏱️ **Sequential Processing:** Serializes background image analysis tasks to prevent client crashes due to parallel AI engine memory usage.
54+
- ⚙️ **Queue Lifecycle Status:** Tracks item-level state machines (`pending`, `processing`, `ready`, `error`).
55+
- 🌐 **Isomorphic & Environment Agnostic:** Loads DOM Images automatically in Browser environments, and supports Buffers/Streams/Paths natively in Node.js.
56+
57+
### Installation
58+
```bash
59+
npm install @craftthingy-digital-innovation/cty-ocr-queue
60+
```
61+
62+
### Usage
63+
```javascript
64+
import { OcrQueueManager } from '@craftthingy-digital-innovation/cty-ocr-queue';
65+
66+
const ocrQueue = new OcrQueueManager({
67+
getOcrClient: async () => {
68+
return myPaddleOCRInstance;
69+
},
70+
onQueueUpdate: (queue) => {
71+
renderSidebar(queue);
72+
}
73+
});
74+
75+
ocrQueue.enqueue({
76+
filename: 'doc.jpg',
77+
url: 'data:image/jpeg;base64,...'
78+
});
79+
```
80+
81+
## License
82+
Licensed under Public-Source Corporate Royalty License (PSCRL). See [LICENSE](./LICENSE) for details.

package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@craftthingy-digital-innovation/cty-ocr-queue",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"repository": {
6+
"type": "git",
7+
"url": "git+https://github.com/CraftThingy-Digital-Innovation/cty-ocr-queue.git"
8+
},
9+
"bugs": {
10+
"url": "https://github.com/CraftThingy-Digital-Innovation/cty-ocr-queue/issues"
11+
},
12+
"homepage": "https://github.com/CraftThingy-Digital-Innovation/cty-ocr-queue#readme",
13+
"main": "./dist/cty-ocr-queue.umd.js",
14+
"module": "./dist/cty-ocr-queue.es.js",
15+
"exports": {
16+
".": {
17+
"import": "./dist/cty-ocr-queue.es.js",
18+
"require": "./dist/cty-ocr-queue.umd.js"
19+
}
20+
},
21+
"files": [
22+
"dist",
23+
"src"
24+
],
25+
"scripts": {
26+
"build": "vite build"
27+
},
28+
"publishConfig": {
29+
"access": "public"
30+
},
31+
"dependencies": {},
32+
"devDependencies": {
33+
"vite": "^5.2.0"
34+
}
35+
}

src/index.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* OcrQueueManager Library
3+
* Part of CraftThingy Digital Innovation SDK
4+
* Licensed under Public-Source Corporate Royalty License (PSCRL)
5+
* Isomorphic: Runs in both Browser and Node.js environments.
6+
*/
7+
8+
export class OcrQueueManager {
9+
constructor(config = {}) {
10+
this.getOcrClient = config.getOcrClient;
11+
this.onQueueUpdate = config.onQueueUpdate || (() => {});
12+
this.onItemReady = config.onItemReady || (() => {});
13+
this.onItemError = config.onItemError || (() => {});
14+
this.onItemProcessing = config.onItemProcessing || (() => {});
15+
16+
this.queue = [];
17+
this.isProcessing = false;
18+
this.activeIndex = -1;
19+
}
20+
21+
/**
22+
* Add new image to the queue and start background OCR runner
23+
* @param {Object} item { id, filename, url } (url can be a URL string, Base64, Node.js Buffer, or Canvas)
24+
*/
25+
enqueue(item = {}) {
26+
const queueItem = {
27+
id: item.id || this._generateId(),
28+
filename: item.filename || `ocr_file_${Date.now()}.jpg`,
29+
url: item.url,
30+
status: 'pending',
31+
results: []
32+
};
33+
34+
this.queue.push(queueItem);
35+
this.onQueueUpdate(this.queue);
36+
37+
// Start consuming queue in the background
38+
this._processNext();
39+
}
40+
41+
/**
42+
* Delete queue item by index
43+
* @param {Number} index
44+
*/
45+
dequeue(index) {
46+
if (index < 0 || index >= this.queue.length) return;
47+
48+
this.queue.splice(index, 1);
49+
50+
if (this.activeIndex === index) {
51+
this.activeIndex = this.queue.length > 0 ? 0 : -1;
52+
} else if (this.activeIndex > index) {
53+
this.activeIndex--;
54+
}
55+
56+
this.onQueueUpdate(this.queue);
57+
this._processNext();
58+
}
59+
60+
/**
61+
* Set active selected queue index
62+
* @param {Number} index
63+
*/
64+
selectItem(index) {
65+
if (index < 0 || index >= this.queue.length) return;
66+
this.activeIndex = index;
67+
this.onQueueUpdate(this.queue);
68+
}
69+
70+
/**
71+
* Get all queue items
72+
*/
73+
getItems() {
74+
return [...this.queue];
75+
}
76+
77+
/**
78+
* Get active selected item
79+
*/
80+
getActiveItem() {
81+
if (this.activeIndex === -1 || this.activeIndex >= this.queue.length) return null;
82+
return this.queue[this.activeIndex];
83+
}
84+
85+
/**
86+
* Get active selected index
87+
*/
88+
getActiveIndex() {
89+
return this.activeIndex;
90+
}
91+
92+
/**
93+
* Clear queue state
94+
*/
95+
clear() {
96+
this.queue = [];
97+
this.activeIndex = -1;
98+
this.isProcessing = false;
99+
this.onQueueUpdate(this.queue);
100+
}
101+
102+
/**
103+
* Process next pending item in the queue sequentially
104+
*/
105+
async _processNext() {
106+
if (this.isProcessing) return;
107+
108+
const pendingIndex = this.queue.findIndex(item => item.status === 'pending');
109+
if (pendingIndex === -1) {
110+
this.isProcessing = false;
111+
return;
112+
}
113+
114+
this.isProcessing = true;
115+
const item = this.queue[pendingIndex];
116+
item.status = 'processing';
117+
this.onQueueUpdate(this.queue);
118+
this.onItemProcessing(item);
119+
120+
try {
121+
const client = await this.getOcrClient();
122+
if (!client) {
123+
throw new Error("OCR engine client not initialized");
124+
}
125+
126+
const isBrowser = typeof window !== 'undefined' && typeof window.Image !== 'undefined';
127+
const isUrlString = typeof item.url === 'string';
128+
129+
if (isBrowser && isUrlString) {
130+
// Browser environment: Load image URL via DOM Image element before sending to client
131+
const img = new Image();
132+
img.onload = async () => {
133+
try {
134+
const result = await client.recognize(img);
135+
this._handleSuccess(result, item, pendingIndex);
136+
} catch (err) {
137+
this._handleError(item, err);
138+
}
139+
};
140+
141+
img.onerror = () => {
142+
this._handleError(item, new Error("Failed to load image element source"));
143+
};
144+
145+
img.src = item.url;
146+
} else {
147+
// Node.js or Buffer environment: Pass URL/Buffer/Canvas directly to the OCR client
148+
try {
149+
const result = await client.recognize(item.url);
150+
this._handleSuccess(result, item, pendingIndex);
151+
} catch (err) {
152+
this._handleError(item, err);
153+
}
154+
}
155+
} catch (err) {
156+
this._handleError(item, err);
157+
}
158+
}
159+
160+
_handleSuccess(result, item, pendingIndex) {
161+
let words = [];
162+
if (result && result.lines) {
163+
result.lines.forEach(line => {
164+
if (line.words) {
165+
words.push(...line.words);
166+
}
167+
});
168+
}
169+
170+
item.results = words;
171+
item.status = 'ready';
172+
173+
// Auto-select first loaded item
174+
if (this.activeIndex === -1) {
175+
this.activeIndex = pendingIndex;
176+
}
177+
178+
this.onItemReady(item, pendingIndex);
179+
this.isProcessing = false;
180+
this.onQueueUpdate(this.queue);
181+
this._processNext();
182+
}
183+
184+
_handleError(item, err) {
185+
item.status = 'error';
186+
this.onItemError(item, err);
187+
this.isProcessing = false;
188+
this.onQueueUpdate(this.queue);
189+
this._processNext();
190+
}
191+
192+
_generateId() {
193+
return Math.random().toString(36).substring(2, 9);
194+
}
195+
}

0 commit comments

Comments
 (0)