diff --git a/CHANGELOG.md b/CHANGELOG.md index b73f27e..ad6b466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Changed +- Hardened the v2.1.0 release candidate with real English, Korean, and combined local OCR smoke coverage plus reproducible browser, privacy, CSP, asset, locale, responsive, accessibility, and regression evidence. - Hardened v2 promotion gates for all ten production tools, vendored-resource integrity, local-only network invariants, save failure paths, and resource boundaries. - Upgraded secure-metadata to v0.1.1 so JPEG Privacy Clean preserves one valid rendering Orientation while removing other targeted EXIF/GPS data without decoding or re-encoding pixels. - Aligned Image and PDF Metadata action panels and bounded their primary decoded summaries while retaining complete details. diff --git a/docs/README.md b/docs/README.md index ee12f55..78464a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,8 @@ The root [README](../README.md) introduces Secure Tools. This directory owns det | [Dependencies](./dependencies.md) | Production runtime inventory, versions, vendoring, licenses, and integrity ownership | | [Local OCR foundation](./ocr-foundation.md) | Self-hosted Tesseract assets, languages, lifecycle, cancellation, caching, and privacy guarantees | | [Sprint 16B Image → Text QA](./sprint-16b-qa.md) | Automated and Chromium browser evidence for the v2.1.0 Image → Text workflow | +| [Sprint 16C v2.1.0 release hardening](./sprint-16c-v2.1-release-hardening.md) | Release-candidate regression, OCR, privacy, browser, performance, and readiness evidence | +| [v2.1.0 release notes draft](./v2.1.0-release-notes-draft.md) | Unpublished release-note copy for the later promotion and release task | | [Tool status](./tool-status.md) | Production and planned surfaces, supported formats, behavior, and resource boundaries | | [Search discovery and metadata](./seo.md) | Canonical routes, crawler files, metadata policy, maintenance, and submission steps | | [Cloudflare Pages migration bridge](./cloudflare-pages-bridge.md) | H3.2/H3.3 provenance plus the prepared H3.5 hostname-specific indexing, activation, validation, and rollback contracts | diff --git a/docs/architecture.md b/docs/architecture.md index 24e0f74..9fac252 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,7 +11,7 @@ Production routes load application code and pinned libraries from the same origi The homepage points to stable category hubs instead of maintaining a flat list of every utility: - PDF: six production tools; -- Image: four production tools; +- Image: five production tools, including Image → Text OCR; - Privacy: a cross-category hub for the two metadata tools; - Scan/OCR and Media: planned, non-interactive surfaces. @@ -44,7 +44,7 @@ The current production inventory and tool-specific behavior live in [tool status - `js/i18n.js` resolves six supported languages, applies translations without reload, updates document metadata and ``, and preserves tool state when language changes. - `js/config.js` centralizes repository links. - `tools/shared/` owns common file admission, signature validation, image/PDF helpers, queue conventions, local save behavior, and shared tool presentation. -- `tools/shared/ocr.js` owns language selection, same-origin OCR paths, normalized progress, orientation-aware image preparation, worker reuse, cancellation, and disposal. It is infrastructure only and is not linked from the public tool surface. +- `tools/shared/ocr.js` owns language selection, same-origin OCR paths, normalized progress, orientation-aware image preparation, worker reuse, cancellation, and disposal for the public Image → Text workflow. - The File System Access API is used when available; a revoking Blob-download fallback serves other browsers. Tool implementations retain specialized models when their workflows differ. Organizer uses a page grid and PDF rendering lifecycle; Metadata tools use bounded inspection models and fail-closed output verification. Shared UI does not erase these tool-specific guarantees. @@ -57,7 +57,7 @@ Image conversion, resizing, and compression use browser decode, Canvas, and enco ## Development and delivery -Serving the committed production tree requires only an HTTP server. Reproducing OCR assets and running the full CI checks requires Node.js 24 and the exact lockfile. `npm run build` verifies prepared OCR assets, `npm test` runs the static and unit suite, and `node tests/ocr-smoke.test.mjs` performs real English recognition. +Serving the committed production tree requires only an HTTP server. Reproducing OCR assets and running the full CI checks requires Node.js 24 and the exact lockfile. `npm run build` verifies prepared OCR assets, `npm test` runs the static and unit suite, and `node tests/ocr-smoke.test.mjs` performs real English, Korean, and combined recognition. `.github/workflows/ci.yml` validates pull requests and pushes to `main` using Node.js 24. It installs the lockfile only to reproduce and verify OCR assets, then checks commit-range whitespace, JavaScript syntax, unit coverage, and real local OCR without adding deployment behavior. diff --git a/docs/ocr-foundation.md b/docs/ocr-foundation.md index 41930f3..0b2bccd 100644 --- a/docs/ocr-foundation.md +++ b/docs/ocr-foundation.md @@ -29,6 +29,8 @@ Production URLs are: The service always passes explicit `workerPath`, `corePath`, and `langPath` values. `corePath` is the directory, so Tesseract.js can choose the scalar, SIMD, or relaxed-SIMD LSTM runtime. `workerBlobURL: false` creates a direct same-origin worker. Missing local assets produce a controlled initialization failure; the application has no CDN or external-service retry. +Browser HTTP cache and Tesseract.js model caching may allow a previously loaded workflow to run while disconnected. Secure Tools does not install a service worker and does not guarantee that application, worker, core, or model assets are available offline. A fresh or partially cached browser can therefore fail with the localized initialization error. There is no cloud fallback. + ## Service behavior `tools/shared/ocr.js` supports `eng`, `kor`, and `eng+kor` internally. UI code should present localized language names rather than these engine identifiers. @@ -45,7 +47,7 @@ The public controller adds a monotonically increasing request identity around th The browser smoke page at `tests/browser/ocr-smoke.html` runs the real browser bundle, direct worker, WASM core, and English model under the same strict meta CSP used by production pages. The public route uses the same runtime paths and unchanged production CSP. Browser QA also exercises the actual Image → Text selection, recognition, edit, copy/download, replace, remove, and cancellation paths. -Run the repeatable browser check with `npm run smoke:ocr:browser`, then open the printed localhost URL and require a visible `PASS` result. The page rejects any third-party resource entry it observes. The automated Node smoke test performs real English recognition using local core and trained data; unit tests cover path configuration, language mapping, progress, orientation cleanup, reuse, language replacement, initialization/recognition failures, cancellation, disposal, and stale callbacks. +Run the repeatable browser check with `npm run smoke:ocr:browser`, then open the printed localhost URL and require a visible `PASS` result. The page rejects any third-party resource entry it observes. The automated Node smoke test performs real English, Korean, and combined recognition using local core and trained data; unit tests cover path configuration, language mapping, progress, orientation cleanup, reuse, language replacement, initialization/recognition failures, cancellation, disposal, and stale callbacks. ## Adding a language diff --git a/docs/sprint-16b-qa.md b/docs/sprint-16b-qa.md index 95b9498..c9cda0b 100644 --- a/docs/sprint-16b-qa.md +++ b/docs/sprint-16b-qa.md @@ -10,7 +10,7 @@ Route: `/tools/image/to-text/` - `npm run build`: pinned OCR assets match the installed packages and lockfile. - `npm test`: syntax, state-machine, stale-callback, localization, route, CSP, privacy, responsive-contract, and regression suites pass. -- `node tests/ocr-smoke.test.mjs`: real local English OCR passes with the pinned core and language data. +- `node tests/ocr-smoke.test.mjs`: real local English, Korean, and combined OCR pass with the pinned core and language data. - The browser smoke page completes real English OCR through the same direct worker, WASM core, and language-model paths and reports `PASS: HELLO`. ## Chromium browser QA diff --git a/docs/sprint-16c-v2.1-release-hardening.md b/docs/sprint-16c-v2.1-release-hardening.md new file mode 100644 index 0000000..726c190 --- /dev/null +++ b/docs/sprint-16c-v2.1-release-hardening.md @@ -0,0 +1,63 @@ +# Sprint 16C v2.1.0 release hardening + +Date: 2026-09-23 + +Candidate: `v2.1` at `74d152a5b79a82abb38ca86d60b3b1b313041dce` + +Production baseline: `main` at `4bc9fb5835d72b1512662d7ca80f2505ba08be00` + +## Baseline and automated checks + +- The baseline working tree was clean, `v2.1` matched `origin/v2.1`, and no open issue or pull request affected the candidate. +- Sprint 16A, Sprint 16B, the development-safety workflow, and branch-policy CI guard were present. +- A clean `npm ci --ignore-scripts` installed the exact lockfile with zero reported vulnerabilities. +- `npm run build`, `npm test`, `node tests/ocr-smoke.test.mjs`, and `git diff --check` passed before hardening. +- The real runtime smoke now recognizes English, Korean, and combined Korean/English fixtures through the pinned local runtime. +- The full suite covers every PDF and Image workflow, both metadata tools, direct routes, localization parity, CSP, SEO/canonical metadata, sitemap, deployment resources, object-URL cleanup, save behavior, and branch policy. + +## OCR runtime, assets, and privacy + +- Pinned packages remain `tesseract.js@7.0.0`, `tesseract.js-core@7.0.0`, and English/Korean data packages at `1.0.0`. +- Explicit worker, core, and language paths resolve under `/assets/vendor/tesseract/`; `workerBlobURL: false` keeps the worker same-origin. No CDN or OCR API fallback is present. +- The prepared inventory contains the engine, worker, both trained-data files, and all 18 scalar/SIMD/relaxed-SIMD core files. Manifest byte lengths and SHA-256 values match every deployed asset, with no duplicate or extra runtime copy. +- The complete committed OCR vendor tree is 49,961,571 bytes. Core variants total 45,244,609 bytes; English data is 2,952,873 bytes; Korean data is 1,572,336 bytes; worker files total 111,773 bytes; engine files total 63,110 bytes. +- Image → Text loads its OCR engine and modules only on its own route. Representative non-OCR pages do not reference the Tesseract runtime. +- Source images, filenames, decoded pixels, recognized text, and history are held only for the active in-memory workflow. Secure Tools does not persist them. Tesseract.js may cache static language-model bytes in IndexedDB; those dependency bytes contain no user content. + +## Chromium browser QA + +QA used the repository static server and the production Image → Text page under its unchanged production CSP. + +- A clean-origin first run selected a real PNG, began combined recognition, cancelled during initialization, reported the localized cancelled state, and retried successfully. +- Combined OCR returned `한글 HELLO`. The result remained editable; clipboard copy preserved mixed Korean/English text; TXT download reported the expected source-derived filename. +- Replacing the source with a 3000 × 2000 PNG and recognizing eight text lines completed correctly. The page did not permanently freeze or lose its source/result state. This constrained browser run took about 14 minutes, so large-image latency remains device-dependent and cancellation is the practical escape path. +- The browser smoke page returned `PASS: HELLO` and would fail on any third-party resource entry. The production workflow produced no console or CSP warning. +- With the local server stopped after prior loading, Chromium reloaded the cached page and completed combined OCR from cached static runtime/model assets. This is observed cache behavior, not a full offline guarantee; a fresh browser without required cached assets cannot rely on offline OCR and receives the existing localized initialization failure. +- No hidden cloud fallback, user upload, analytics, telemetry, or third-party OCR request exists. + +## Locale, theme, responsive, and accessibility QA + +- English, Korean, Japanese, Spanish, German, and French each rendered the localized Image → Text heading with zero raw translation keys. Automated parity covers all 817 keys, placeholders, metadata, controls, errors, status text, and accessibility labels. +- System, Light, and Dark selected distinct resolved theme values without changing workflow state. +- At 375 × 812 the document width stayed within the viewport and the picker, recognition action, and result textarea remained visible and usable. The tablet check at 768 pixels also had no horizontal overflow. +- The browser accessibility tree exposed the file picker, language selector, result editor, cancel/retry, copy, and download controls with names in the active locale. Unit/static checks cover live status, busy/disabled state, logical native controls, visible focus rules, and reduced-motion rules. + +## Existing-tool and discovery regression + +- Direct Chromium navigation loaded Images → PDF, PDF Merge, PDF Split, PDF Organizer, PDF → Images, PDF Metadata, Image Converter, Resize, Compress, and Image Metadata with their file controls and localized headings. +- Automated workflow tests exercise actual processing, cancellation/error paths, signature validation, output generation, and metadata verification for those tools. +- Image → Text has one canonical route, localized title/description metadata, Open Graph metadata, sitemap membership, and Image-category discovery. No duplicate canonical route was found. + +## Findings + +| Severity | Finding | Status | +| --- | --- | --- | +| MEDIUM | Architecture documentation still described four Image tools and called OCR non-public after Sprint 16B. | Fixed: inventory and public OCR ownership now match the implementation. | +| LOW | The real runtime smoke covered English only, leaving Korean and combined model execution to unit/browser evidence. | Fixed: deterministic Korean/English fixture now exercises `kor` and `eng+kor`. | +| LOW | A 3000 × 2000 OCR run took about 14 minutes in the constrained Chromium QA profile. | Open limitation: operation completed correctly; progress/cancel remain available and latency depends on image and device. No arbitrary file limit was introduced. | + +No BLOCKER or HIGH issue was found. No CSP or dependency change was required. + +## Release readiness + +**GO.** The v2.1 candidate has no remaining release blocker and is ready for a separate release-promotion pull request into `main`. This hardening Sprint does not create that PR, a tag, or a GitHub Release. diff --git a/docs/v2.1.0-release-notes-draft.md b/docs/v2.1.0-release-notes-draft.md new file mode 100644 index 0000000..26eeac4 --- /dev/null +++ b/docs/v2.1.0-release-notes-draft.md @@ -0,0 +1,11 @@ +# Secure Tools v2.1.0 release notes — draft + +Status: unpublished. Use this draft during the separate v2.1.0 promotion and release task. + +Secure Tools v2.1.0 adds Image → Text, a local OCR workflow for one PNG, JPEG, or WebP image. Recognition supports English, Korean, and combined English + Korean, with editable results, clipboard copy, and UTF-8 text download. + +The OCR engine, worker, WebAssembly core, and trained language data are pinned and served from the Secure Tools origin. Images and recognized text stay in the browser, no OCR server or analytics endpoint is used, and Secure Tools stores no OCR history. The browser may cache static language-model data for performance; that cache contains no user image or recognized text. + +The six interface locales remain English, Korean, Japanese, Spanish, German, and French. This release also hardens cancellation and retry, stale-result protection, worker and preview cleanup, production asset integrity, Content Security Policy compatibility, responsive behavior, accessibility, and existing PDF and image-tool regression coverage. + +PDF OCR, searchable PDF output, batch OCR, camera capture, and additional recognition languages are outside v2.1.0. diff --git a/tests/fixtures/ocr-korean-english.png.b64 b/tests/fixtures/ocr-korean-english.png.b64 new file mode 100644 index 0000000..14d08b2 --- /dev/null +++ b/tests/fixtures/ocr-korean-english.png.b64 @@ -0,0 +1 @@ +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 diff --git a/tests/ocr-foundation.test.mjs b/tests/ocr-foundation.test.mjs index c9fb97a..b11f2bd 100644 --- a/tests/ocr-foundation.test.mjs +++ b/tests/ocr-foundation.test.mjs @@ -200,6 +200,17 @@ assert.deepEqual( [...Object.keys(manifest.assets), "README.md", "manifest.json"].sort(), "OCR vendor inventory contains only documented prepared assets", ); +function listAbsoluteFiles(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const target = path.join(directory, entry.name); + return entry.isDirectory() ? listAbsoluteFiles(target) : [target]; + }); +} +const publicOcrReferences = listAbsoluteFiles(path.join(root, "tools")) + .filter((file) => file.endsWith(".html")) + .map((file) => path.relative(root, file).replaceAll("\\", "/")) + .filter((relative) => read(relative).includes("assets/vendor/tesseract")); +assert.deepEqual(publicOcrReferences, ["tools/image/to-text/index.html"], "OCR runtime must stay lazy to its public route"); for (const required of ["engine/tesseract.min.js", "worker/worker.min.js", "lang/eng.traineddata.gz", "lang/kor.traineddata.gz"]) { assert.ok(manifest.assets[required], `missing ${required}`); } diff --git a/tests/ocr-smoke.test.mjs b/tests/ocr-smoke.test.mjs index 61b2f3c..73895a7 100644 --- a/tests/ocr-smoke.test.mjs +++ b/tests/ocr-smoke.test.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import fs from "node:fs"; import path from "node:path"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; @@ -58,21 +59,36 @@ function createTextBmp(text) { return Buffer.concat([header, pixels]); } -const progress = []; -const worker = await createWorker("eng", 1, { - langPath: path.join(root, "assets", "vendor", "tesseract", "lang"), - cacheMethod: "none", - logger(message) { progress.push(message.status); }, -}); +async function recognize(language, image) { + const progress = []; + const worker = await createWorker(language, 1, { + langPath: path.join(root, "assets", "vendor", "tesseract", "lang"), + cacheMethod: "none", + logger(message) { progress.push(message.status); }, + }); -try { - const result = await worker.recognize(createTextBmp("HELLO")); - assert.match(result.data.text.replace(/\s+/g, " ").trim(), /HELLO/i); - assert.ok(progress.includes("loading tesseract core")); - assert.ok(progress.includes("loading language traineddata")); - assert.ok(progress.includes("recognizing text")); -} finally { - await worker.terminate(); + try { + const result = await worker.recognize(image); + return { text: result.data.text.replace(/\s+/g, " ").trim(), progress }; + } finally { + await worker.terminate(); + } } -console.log("Real local English OCR smoke test passed."); +const english = await recognize("eng", createTextBmp("HELLO")); +assert.match(english.text, /HELLO/i); +assert.ok(english.progress.includes("loading tesseract core")); +assert.ok(english.progress.includes("loading language traineddata")); +assert.ok(english.progress.includes("recognizing text")); + +const multilingualFixture = Buffer.from( + fs.readFileSync(path.join(root, "tests", "fixtures", "ocr-korean-english.png.b64"), "utf8").trim(), + "base64", +); +const korean = await recognize("kor", multilingualFixture); +assert.match(korean.text, /한글/); +const combined = await recognize("eng+kor", multilingualFixture); +assert.match(combined.text, /한글/); +assert.match(combined.text, /HELLO/i); + +console.log("Real local English, Korean, and combined OCR smoke tests passed.");