hh-shipments 0.3.24 → 0.3.25
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 +101 -35
- package/public/index.html +1 -1
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -175,53 +175,120 @@ function loadTesseract() {
|
|
|
175
175
|
return tesseractLoading;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
//
|
|
179
|
-
// screenshots (dark
|
|
180
|
-
//
|
|
181
|
-
|
|
178
|
+
// "Better eyes": smart OCR pipeline that adapts to image type.
|
|
179
|
+
// - Chat screenshots (dark): invert, PSM 6 (uniform block)
|
|
180
|
+
// - Labels (light): crop to tracking zone, upscale, PSM 6
|
|
181
|
+
// - Falls back to full-image PSM 3 if targeted fails
|
|
182
|
+
async function ocrFile(file, statusEl) {
|
|
183
|
+
await loadTesseract();
|
|
184
|
+
statusEl.textContent = 'Reading screenshot…';
|
|
185
|
+
const isDark = await isDarkImage(file);
|
|
186
|
+
file = isDark ? await invertImage(file) : file;
|
|
187
|
+
|
|
188
|
+
const log = (m) => { if (m.status === 'recognizing text' && statusEl) statusEl.textContent = `Reading… ${Math.round(m.progress * 100)}%`; };
|
|
189
|
+
|
|
190
|
+
// Pass 1: targeted — for labels, crop to the tracking-number zone (bottom 45%)
|
|
191
|
+
// and upscale for Tesseract. For chats, use the full image.
|
|
192
|
+
let bestText = '';
|
|
193
|
+
if (!isDark) {
|
|
194
|
+
try {
|
|
195
|
+
const zoned = await cropAndUpscale(file, 0, 0.55, 1, 0.45, 2);
|
|
196
|
+
const r1 = await Tesseract.recognize(zoned, 'eng', {
|
|
197
|
+
logger: log,
|
|
198
|
+
tessedit_pageseg_mode: '6', // uniform block of text
|
|
199
|
+
});
|
|
200
|
+
bestText = r1.data.text || '';
|
|
201
|
+
// If the zone has a plausible tracking number, we're done
|
|
202
|
+
if (/(1Z|12|IZ|17)\s*[0-9A-Z)\]}(\[{|!]/i.test(bestText)) {
|
|
203
|
+
if (statusEl) statusEl.textContent = '';
|
|
204
|
+
return bestText;
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Pass 2: full image with PSM 6
|
|
210
|
+
try {
|
|
211
|
+
const r2 = await Tesseract.recognize(file, 'eng', {
|
|
212
|
+
logger: log,
|
|
213
|
+
tessedit_pageseg_mode: '6',
|
|
214
|
+
});
|
|
215
|
+
const t2 = r2.data.text || '';
|
|
216
|
+
if (t2.length > bestText.length) bestText = t2;
|
|
217
|
+
} catch (e) {}
|
|
218
|
+
|
|
219
|
+
// Pass 3: fallback to default PSM 3 if we got nothing useful
|
|
220
|
+
if (!bestText || bestText.trim().length < 10) {
|
|
221
|
+
try {
|
|
222
|
+
const r3 = await Tesseract.recognize(file, 'eng', { logger: log });
|
|
223
|
+
const t3 = r3.data.text || '';
|
|
224
|
+
if (t3.length > bestText.length) bestText = t3;
|
|
225
|
+
} catch (e) {}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (statusEl) statusEl.textContent = '';
|
|
229
|
+
return bestText;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function isDarkImage(file) {
|
|
182
233
|
try {
|
|
183
234
|
const bmp = await createImageBitmap(file);
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
canvas.width = sw; canvas.height = sh;
|
|
189
|
-
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
235
|
+
const sw = Math.min(100, bmp.width), sh = Math.min(100, bmp.height);
|
|
236
|
+
const c = document.createElement('canvas');
|
|
237
|
+
c.width = sw; c.height = sh;
|
|
238
|
+
const ctx = c.getContext('2d', { willReadFrequently: true });
|
|
190
239
|
ctx.drawImage(bmp, 0, 0, sw, sh);
|
|
191
240
|
const px = ctx.getImageData(0, 0, sw, sh).data;
|
|
192
241
|
let sum = 0, n = 0;
|
|
193
242
|
for (let i = 0; i < px.length; i += 16) {
|
|
194
|
-
sum += 0.299 * px[i] + 0.587 * px[i
|
|
243
|
+
sum += 0.299 * px[i] + 0.587 * px[i+1] + 0.114 * px[i+2];
|
|
195
244
|
n++;
|
|
196
245
|
}
|
|
197
|
-
const avg = sum / n;
|
|
198
246
|
bmp.close();
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
247
|
+
return (sum / n) < 110;
|
|
248
|
+
} catch (e) { return false; }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function invertImage(file) {
|
|
252
|
+
try {
|
|
253
|
+
const bmp = await createImageBitmap(file);
|
|
254
|
+
const c = document.createElement('canvas');
|
|
255
|
+
c.width = bmp.width; c.height = bmp.height;
|
|
256
|
+
const ctx = c.getContext('2d');
|
|
257
|
+
ctx.drawImage(bmp, 0, 0);
|
|
258
|
+
bmp.close();
|
|
259
|
+
const img = ctx.getImageData(0, 0, c.width, c.height);
|
|
208
260
|
const d = img.data;
|
|
209
261
|
for (let i = 0; i < d.length; i += 4) {
|
|
210
|
-
d[i] = 255 - d[i]; d[i
|
|
262
|
+
d[i] = 255 - d[i]; d[i+1] = 255 - d[i+1]; d[i+2] = 255 - d[i+2];
|
|
211
263
|
}
|
|
212
|
-
|
|
213
|
-
const blob = await new Promise((res) =>
|
|
264
|
+
ctx.putImageData(img, 0, 0);
|
|
265
|
+
const blob = await new Promise((res) => c.toBlob(res, 'image/jpeg', 0.92));
|
|
214
266
|
return blob ? new File([blob], 'inv.jpg', { type: 'image/jpeg' }) : file;
|
|
215
267
|
} catch (e) { return file; }
|
|
216
268
|
}
|
|
217
269
|
|
|
218
|
-
async function
|
|
219
|
-
await
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
270
|
+
async function cropAndUpscale(file, fx, fy, fw, fh, scale) {
|
|
271
|
+
const bmp = await createImageBitmap(file);
|
|
272
|
+
const sw = Math.round(bmp.width * fw), sh = Math.round(bmp.height * fh);
|
|
273
|
+
const c = document.createElement('canvas');
|
|
274
|
+
c.width = Math.round(sw * scale);
|
|
275
|
+
c.height = Math.round(sh * scale);
|
|
276
|
+
const ctx = c.getContext('2d');
|
|
277
|
+
ctx.drawImage(bmp, bmp.width * fx, bmp.height * fy, sw, sh, 0, 0, c.width, c.height);
|
|
278
|
+
bmp.close();
|
|
279
|
+
// Boost contrast for text
|
|
280
|
+
try {
|
|
281
|
+
const img = ctx.getImageData(0, 0, c.width, c.height);
|
|
282
|
+
const d = img.data;
|
|
283
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
284
|
+
const v = 0.299*d[i] + 0.587*d[i+1] + 0.114*d[i+2];
|
|
285
|
+
const nv = v < 140 ? Math.max(0, v - 40) : Math.min(255, v + 40);
|
|
286
|
+
d[i] = d[i+1] = d[i+2] = nv;
|
|
287
|
+
}
|
|
288
|
+
ctx.putImageData(img, 0, 0);
|
|
289
|
+
} catch (e) {}
|
|
290
|
+
const blob = await new Promise((res) => c.toBlob(res, 'image/jpeg', 0.95));
|
|
291
|
+
return new File([blob], 'crop.jpg', { type: 'image/jpeg' });
|
|
225
292
|
}
|
|
226
293
|
|
|
227
294
|
async function handlePhotoFile(f) {
|
|
@@ -533,13 +600,12 @@ async function loadPending() {
|
|
|
533
600
|
async function reviewPendingImage(id) {
|
|
534
601
|
$('photo-result').innerHTML = `<img src="/api/intake/image/${id}" style="max-width:100%;border-radius:10px"><p class="muted">Extracting…</p>`;
|
|
535
602
|
try {
|
|
536
|
-
await loadTesseract();
|
|
537
603
|
const resp = await fetch(`/api/intake/image/${id}`);
|
|
538
604
|
const blob = await resp.blob();
|
|
539
|
-
const file =
|
|
540
|
-
const
|
|
605
|
+
const file = new File([blob], 'pending.jpg', { type: blob.type || 'image/jpeg' });
|
|
606
|
+
const text = await ocrFile(file, null);
|
|
541
607
|
await api.patch('/api/intake/' + id, { state: 'reviewed' });
|
|
542
|
-
await runQuick(
|
|
608
|
+
await runQuick(text, $('photo-result'));
|
|
543
609
|
loadPending();
|
|
544
610
|
} catch (e) { $('photo-result').innerHTML = `<p class="error">${esc(e.message)}</p>`; }
|
|
545
611
|
}
|
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.25">
|
|
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">
|