diff --git a/CHANGELOG.md b/CHANGELOG.md index edac168..57c9eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Images are downscaled to 1600 px on the long edge before OCR. A 12 MP phone photo (~4000 px) drops to ~1.3 MP, making Tesseract 4-8× faster with no real accuracy loss for printed text. Smaller-than-1600 px inputs are passed through unchanged. - Adaptive thresholding (Bradley's method with integral images) binarises the downscaled image before OCR — drops hallucinated text from shadows, gradients, and surface texture on phone photos. Output is pure black-on-white, much closer to the scanned-page input Tesseract was trained on. - Per-word confidence filter (≥ 60) drops the low-confidence noise Tesseract still emits. Re-assembles lines from kept words; status line reports how many words were dropped. +- Crop & rotate modal between image pick and OCR. After choosing an image, a modal pops up with cropperjs's drag-handle crop UI and Rotate left / Rotate right / Reset buttons. EXIF orientation is honoured automatically. Cancel / `Esc` / clicking the backdrop aborts; `Enter` accepts. Library lazy-loaded from esm.sh (no bundling). ### Fixed - "Hot: …" status no longer claims "unloads in X" when the server's `keep_alive` is `-1` (Ollama returns an absurd year-4001 `expires_at`). Threshold: any expiry >7 days drops the suffix and just shows the model name. diff --git a/static/index.html b/static/index.html index 7ddd98c..80a8aa2 100644 --- a/static/index.html +++ b/static/index.html @@ -190,6 +190,74 @@ } .scan-status.error { color: var(--error); } + /* Crop / rotate modal (cropperjs lives inside .modal-body) */ + .modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.78); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 1000; + } + .modal { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + width: min(90vw, 900px); + max-height: 90vh; + display: flex; + flex-direction: column; + } + .modal-header { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border); + font-size: 0.85rem; + color: var(--muted); + letter-spacing: 0.04em; + } + .modal-body { + flex: 1; + min-height: 0; + max-height: 70vh; + background: #000; + } + .modal-body img { display: block; max-width: 100%; } + .modal-footer { + padding: 0.75rem 1rem; + border-top: 1px solid var(--border); + display: flex; + gap: 0.5rem; + justify-content: space-between; + flex-wrap: wrap; + } + .modal-footer .group { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + .modal-btn { + background: transparent; + border: 1px solid var(--border); + color: var(--muted); + padding: 0.4rem 0.9rem; + border-radius: 4px; + cursor: pointer; + margin: 0; + width: auto; + font-size: 0.75rem; + letter-spacing: 0.04em; + text-transform: uppercase; + } + .modal-btn:hover { border-color: var(--accent); color: var(--fg); } + .modal-btn.primary { + background: var(--accent); + color: #fff; + border-color: var(--accent); + } + .modal-btn.primary:hover { color: #fff; } + /* CodeMirror editor host — matches .output styling so input & output align */ .cm-editor { min-height: 180px; @@ -329,6 +397,95 @@ scanStatus.classList.toggle("error", error); } + // Crop + rotate modal. Lazy-loads cropperjs + its CSS from esm.sh on + // first use. Resolves with a Blob (cropped, rotation baked in) or null + // if the user cancelled. + let cropperCssLoaded = false; + async function openCropper(file) { + if (!cropperCssLoaded) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "https://esm.sh/cropperjs@1/dist/cropper.min.css"; + document.head.appendChild(link); + cropperCssLoaded = true; + } + const { default: Cropper } = await import("https://esm.sh/cropperjs@1"); + + return new Promise((resolve) => { + const backdrop = document.createElement("div"); + backdrop.className = "modal-backdrop"; + const modal = document.createElement("div"); + modal.className = "modal"; + const url = URL.createObjectURL(file); + modal.innerHTML = ` +