hh-shipments 0.3.19 → 0.3.21
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.
- package/package.json +1 -1
- package/public/app.js +41 -0
- package/public/index.html +1 -1
- package/src/extract.js +11 -13
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -168,9 +168,50 @@ function loadTesseract() {
|
|
|
168
168
|
return tesseractLoading;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// Tesseract reads dark-text-on-light far better than the reverse. Chat
|
|
172
|
+
// screenshots (dark mode) come out as white-on-black, so detect dark images
|
|
173
|
+
// and invert them before OCR. Light images (labels) pass through untouched.
|
|
174
|
+
async function maybeInvertForOcr(file) {
|
|
175
|
+
try {
|
|
176
|
+
const bmp = await createImageBitmap(file);
|
|
177
|
+
const w = bmp.width, h = bmp.height;
|
|
178
|
+
const canvas = document.createElement('canvas');
|
|
179
|
+
// sample at reduced size for speed
|
|
180
|
+
const sw = Math.min(200, w), sh = Math.min(200, h);
|
|
181
|
+
canvas.width = sw; canvas.height = sh;
|
|
182
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
183
|
+
ctx.drawImage(bmp, 0, 0, sw, sh);
|
|
184
|
+
const px = ctx.getImageData(0, 0, sw, sh).data;
|
|
185
|
+
let sum = 0, n = 0;
|
|
186
|
+
for (let i = 0; i < px.length; i += 16) {
|
|
187
|
+
sum += 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2];
|
|
188
|
+
n++;
|
|
189
|
+
}
|
|
190
|
+
const avg = sum / n;
|
|
191
|
+
bmp.close();
|
|
192
|
+
if (avg > 110) return file; // already light — leave it alone
|
|
193
|
+
// dark image: invert to dark-on-light
|
|
194
|
+
const c2 = document.createElement('canvas');
|
|
195
|
+
c2.width = w; c2.height = h;
|
|
196
|
+
const x2 = c2.getContext('2d');
|
|
197
|
+
const full = await createImageBitmap(file);
|
|
198
|
+
x2.drawImage(full, 0, 0);
|
|
199
|
+
full.close();
|
|
200
|
+
const img = x2.getImageData(0, 0, w, h);
|
|
201
|
+
const d = img.data;
|
|
202
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
203
|
+
d[i] = 255 - d[i]; d[i + 1] = 255 - d[i + 1]; d[i + 2] = 255 - d[i + 2];
|
|
204
|
+
}
|
|
205
|
+
x2.putImageData(img, 0, 0);
|
|
206
|
+
const blob = await new Promise((res) => c2.toBlob(res, 'image/jpeg', 0.92));
|
|
207
|
+
return blob ? new File([blob], 'inv.jpg', { type: 'image/jpeg' }) : file;
|
|
208
|
+
} catch (e) { return file; }
|
|
209
|
+
}
|
|
210
|
+
|
|
171
211
|
async function ocrFile(file, statusEl) {
|
|
172
212
|
await loadTesseract();
|
|
173
213
|
statusEl.textContent = 'Reading screenshot…';
|
|
214
|
+
file = await maybeInvertForOcr(file);
|
|
174
215
|
const { data } = await Tesseract.recognize(file, 'eng', { logger: (m) => { if (m.status === 'recognizing text') statusEl.textContent = `Reading… ${Math.round(m.progress * 100)}%`; } });
|
|
175
216
|
statusEl.textContent = '';
|
|
176
217
|
return data.text || '';
|
package/public/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
6
|
<meta name="theme-color" content="#0a84ff">
|
|
7
|
-
<meta name="app-version" content="0.3.
|
|
7
|
+
<meta name="app-version" content="0.3.21">
|
|
8
8
|
<title>HH Shipments</title>
|
|
9
9
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
10
10
|
<link rel="apple-touch-icon" href="/icon-192.png">
|
package/src/extract.js
CHANGED
|
@@ -90,25 +90,23 @@
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
// Chat screenshots (Messenger): the other person's name is the header at
|
|
93
|
-
// the very top ("Abdullah Sohail"), above the message bubbles.
|
|
94
|
-
//
|
|
95
|
-
//
|
|
93
|
+
// the very top ("Abdullah Sohail"), above the message bubbles. OCR wraps
|
|
94
|
+
// it in garbage ("{® Abdullah Sohail LC Be"), so find the name *within*
|
|
95
|
+
// the top lines instead of requiring a clean line. Never Hunter's own
|
|
96
|
+
// name/address bubble, never app or chat UI words.
|
|
96
97
|
if (!out.seller_name) {
|
|
97
98
|
var cheadBlock = /\b(UPS|FEDEX|USPS|DHL|GROUND|EXPRESS|PRIORITY|BILLING|TRACKING|POSTAGE|LABEL|SHIPMENT|SHIP\b|SCREENSHOT|READING|FINDING|UPLOADING|EXTRACT|PASTE|DETAILS|SAVE|TAP|CLIPBOARD|NUMBER|BACK|HOME|ARCHIVE|SETTINGS|HUNTER|HILLS|THANK|THANKS|YOU|DELIVERED|SEEN|TYPING|AFTERMARKET|DISPLAY)\b/i;
|
|
98
99
|
var clines = String(text).split(/\r?\n/);
|
|
99
100
|
var checked = 0;
|
|
100
|
-
for (var ci = 0; ci < clines.length && checked <
|
|
101
|
-
var ch = clines[ci]
|
|
102
|
-
if (!ch || ch.length >
|
|
101
|
+
for (var ci = 0; ci < clines.length && checked < 8; ci++) {
|
|
102
|
+
var ch = clines[ci];
|
|
103
|
+
if (!ch || !ch.trim() || ch.length > 80) continue;
|
|
103
104
|
checked++;
|
|
104
105
|
if (/\d/.test(ch)) continue;
|
|
105
|
-
var
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!cok) continue;
|
|
110
|
-
if (cheadBlock.test(ch)) continue;
|
|
111
|
-
out.seller_name = ch;
|
|
106
|
+
var nm = ch.match(/\b([A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,}){1,2})\b/);
|
|
107
|
+
if (!nm) continue;
|
|
108
|
+
if (cheadBlock.test(nm[1])) continue;
|
|
109
|
+
out.seller_name = nm[1];
|
|
112
110
|
break;
|
|
113
111
|
}
|
|
114
112
|
}
|