Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Added the Sprint 16B Image → Text OCR interface for the v2.1.0 development cycle with single-image PNG/JPEG/WebP input, English/Korean/combined recognition, editable results, copy, UTF-8 TXT download, cancellation, retry, orientation-correct previews, request-identity safeguards, and complete six-locale UI coverage.
- Added the internal Sprint 16A local OCR foundation with pinned, same-origin Tesseract.js 7.0.0 worker/core assets, English and Korean language data, orientation-aware image preparation, normalized progress, worker reuse, cancellation, cleanup, and real OCR smoke coverage. No public OCR tool was added.
- Started the v2 cycle with a production Image category and local Image Converter.
- Added Image Resize with pixel and percentage modes, aspect-ratio preservation, optional enlargement, Original/JPEG/PNG/WebP output, and local batch ZIP saving.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ npm test
node tests/ocr-smoke.test.mjs
```

The public Image → Text interface is not part of the current production surface. The reusable local OCR runtime is documented in [Local OCR foundation](./docs/ocr-foundation.md).
The Image category includes a public, single-image [Image → Text OCR](./tools/image/to-text/) workflow for PNG, JPEG, and WebP input. English, Korean, and combined English + Korean recognition run through the same-origin OCR runtime documented in [Local OCR foundation](./docs/ocr-foundation.md).

## Documentation

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The root [README](../README.md) introduces Secure Tools. This directory owns det
| [Privacy model](./privacy-model.md) | Local-processing and network boundaries, storage, security controls, and bounded privacy claims |
| [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 |
| [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 |
Expand Down
8 changes: 5 additions & 3 deletions docs/ocr-foundation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Scope and privacy

Sprint 16A provides reusable image OCR infrastructure without publishing an Image → Text tool. It accepts the same signature-validated PNG, JPEG, and WebP formats as the existing image pipeline and rasterizes them through the shared orientation-aware decoder before recognition. PDF rendering, searchable PDF output, camera capture, batch UI, OCR history, cloud OCR, and text post-processing are outside this foundation.
Sprint 16A provides the reusable OCR infrastructure. Sprint 16B publishes it as a single-image Image → Text tool in the v2.1.0 development cycle. It accepts the same signature-validated PNG, JPEG, and WebP formats as the existing image pipeline and rasterizes them through the shared orientation-aware decoder before recognition. PDF rendering, searchable PDF output, camera capture, batch UI, OCR history, cloud OCR, and automatic text post-processing remain outside this scope.

**User images and OCR output remain in the browser and are not sent to an OCR server.**

Expand Down Expand Up @@ -35,13 +35,15 @@ The service always passes explicit `workerPath`, `corePath`, and `langPath` valu

One service instance reuses a ready worker while the selected language stays the same. Changing the language terminates that worker and creates a replacement. Recognition failure discards the worker so the next request starts cleanly. `dispose()` is idempotent and terminates the owned worker.

Progress callbacks receive only project-owned stages: `loading-engine`, `loading-language`, `initializing`, `recognizing`, and `complete`. Numeric upstream progress is clamped to `0..1`; missing progress remains `null`. Raw logger objects are never exposed, and callbacks are cleared after each operation.
Progress callbacks receive only project-owned stages: `loading-engine`, `loading-language`, `initializing`, `recognizing`, and `complete`. Numeric upstream progress is clamped to `0..1`; missing progress remains `null`. The Image → Text page renders missing numeric progress as indeterminate instead of inventing a percentage. Raw logger objects are never exposed, and callbacks are cleared after each operation.

Tesseract.js does not expose safe per-job cancellation. During recognition, an abort terminates and discards the worker before the promise rejects with `OCR_CANCELLED`; later work creates a new worker. An abort during initialization is observed as soon as the library yields the worker handle, which is then terminated before cancellation returns. This avoids reporting cancellation while an owned worker continues running.

The public controller adds a monotonically increasing request identity around the service. Only the current source, language, and recognition request may update progress or results. Replacing or removing a source, changing language, cancelling, or leaving the page invalidates earlier callbacks. The six visible phases are `empty`, `ready`, `recognizing`, `success`, `error`, and `cancelled`.

## CSP and verification

The unlinked 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. It passed in Chromium with the existing `script-src 'self'`, inherited `worker-src 'self'`, and `connect-src 'none'` policy, without CSP or console errors. No production CSP was changed.
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.

Expand Down
32 changes: 32 additions & 0 deletions docs/sprint-16b-qa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Sprint 16B Image → Text QA

Date: 2026-09-23

Development cycle: v2.1.0

Route: `/tools/image/to-text/`

## Automated evidence

- `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.
- 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

Completed in the Chromium-based Codex in-app browser against the repository’s local static server.

- Selected a production PNG through the actual public Image → Text file input and confirmed an orientation-normalized preview, source name, type, and size.
- Ran real English OCR through the production page and received editable recognized text.
- Edited the result to mixed English/Korean text, copied it successfully, and triggered the UTF-8 `.txt` download path with the source-derived filename.
- Cancelled combined English + Korean recognition while language data was loading, confirmed the explicit cancelled state, then retried successfully.
- Confirmed replacing language or starting a new request clears the earlier result; automated delayed-callback tests cover completion, cancellation, source replacement, and language-change races.
- Confirmed the empty state hides the source card, result panel, progress element, and cancel action. This caught and fixed a CSS `hidden`-attribute regression during QA.
- Switched through English, Korean, Japanese, Spanish, German, and French and confirmed the localized heading, controls, status text, document language, and page metadata update without reload.
- Reloaded with Korean selected and confirmed the preferred OCR default is English + Korean. Reloaded with Japanese selected and confirmed the preferred default returns to English.
- Switched through System, Light, and Dark themes. Confirmed resolved theme values and readable styled surfaces.
- Checked a 375 × 812 viewport: the page remained styled, the tool stacked responsively, and document width matched viewport width without horizontal page overflow.
- Confirmed no browser console warnings or errors in the final styled pass. The real OCR smoke recorded no third-party resource request.

PDF OCR and searchable PDF output remain deferred. This QA does not create a release or tag.
9 changes: 7 additions & 2 deletions docs/tool-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
| Image | [Image Resize](../tools/image/resize/) | Production | Pixel or percentage batch resize |
| Image | [Image Compressor](../tools/image/compress/) | Production | JPEG/WebP quality and PNG re-encoding |
| Image | [Image Metadata Inspector & Cleaner](../tools/image/metadata/) | Production | Supported JPEG, PNG, WebP metadata |
| Image | [Image → Text OCR](../tools/image/to-text/) | Production | One PNG, JPEG, or WebP → editable English/Korean text |
| Privacy | [Privacy hub](../tools/privacy/) | Production hub | Navigation to Image and PDF metadata tools |
| Scan/OCR | Category surface | Planned | No production processing tool |
| Media | Category surface | Planned | No production processing tool |

“Production” means linked and covered by the current repository validation. “Planned” cards are non-interactive. Secure Tools may later link separately deployed companion applications, but no Companion status or integration exists on the live site today.

The Scan/OCR category now has an internal, tested OCR runtime foundation for JPEG, PNG, and WebP input with English, Korean, and combined recognition. It remains planned because Sprint 16A does not expose an Image → Text page or change navigation, sitemap, structured data, or the product catalog.
The Image category exposes the tested OCR runtime through a single-image Image → Text workflow with English, Korean, and combined recognition. The broader Scan/OCR category remains planned; PDF OCR and searchable PDF output are deferred.

## Shared Image boundaries

Expand All @@ -46,6 +47,10 @@ Preserves oriented pixel dimensions while re-encoding in Original/JPEG/PNG/WebP

Accepts one JPEG, PNG, or WebP file and uses the pinned `secure-metadata v0.1.1` browser artifact without decoding or re-encoding pixels. It separates decoded fields from opaque detected containers and presents partial inspection as non-exhaustive. Privacy Clean and Customize operate only on supported metadata classes; verification must pass before output is saved. Valid unambiguous JPEG EXIF Orientation and ICC color information are preserved according to policy. Full guarantees and exclusions are in [Image Metadata privacy and verification](./image-metadata-privacy.md).

### Image → Text OCR

Accepts one signature-validated PNG, JPEG, or WebP image and recognizes English, Korean, or combined text with the pinned same-origin Tesseract.js runtime. The preview and OCR input use the shared orientation-aware decoder. Recognition starts only after an explicit action and reports real engine progress; it can be cancelled and retried. Results remain editable and can be copied or downloaded as UTF-8 text. Source replacement, removal, language changes, cancellation, and navigation invalidate older requests and release owned preview URLs and workers.

## PDF tools

### Images to PDF
Expand Down Expand Up @@ -74,7 +79,7 @@ Inspects Title, Author, Subject, Keywords, Creator, Producer, Creation Date, and

## Deferred directions

Broader PDF modification, compression, encryption, XMP/structural sanitization, Scan/OCR, Media, and offline/PWA work remain deferred. Separately deployed companion applications are an architectural possibility, not a current product status or integration.
Broader PDF modification, compression, encryption, XMP/structural sanitization, PDF OCR, searchable PDF output, broader Scan/OCR workflows, Media, and offline/PWA work remain deferred. Separately deployed companion applications are an architectural possibility, not a current product status or integration.

## Related guarantees

Expand Down
8 changes: 5 additions & 3 deletions js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ import { fr } from "./locales/fr.js";
import { imageResizeLocales } from "./locales/image-resize.js";
import { imageCompressorLocales } from "./locales/image-compressor.js";
import { imageMetadataLocales } from "./locales/image-metadata.js";
import { imageToTextLocales } from "./locales/image-to-text.js";
import { privacyHubLocales } from "./locales/privacy-hub.js";
import { metadataUxLocales } from "./locales/metadata-ux.js";

const STORAGE_KEY = "secure-tools-language";
const baseTranslations = { en, ko, ja, es, de, fr };
export const translations = Object.fromEntries(Object.entries(baseTranslations).map(([language, catalog]) => [language, {
...catalog,
metadata: { ...catalog.metadata, imageResize: imageResizeLocales[language].metadata, imageCompressor: imageCompressorLocales[language].metadata, imageMetadata: imageMetadataLocales[language].metadata, privacyCategory: privacyHubLocales[language].metadata },
tools: { ...catalog.tools, imageMetadata: imageMetadataLocales[language].toolName, categoryDescriptions: { ...catalog.tools.categoryDescriptions, privacy: privacyHubLocales[language].categoryDescription } },
categories: { ...catalog.categories, image: { ...catalog.categories.image, metadata: imageMetadataLocales[language].categoryDescription } },
metadata: { ...catalog.metadata, imageResize: imageResizeLocales[language].metadata, imageCompressor: imageCompressorLocales[language].metadata, imageMetadata: imageMetadataLocales[language].metadata, imageToText: imageToTextLocales[language].metadata, privacyCategory: privacyHubLocales[language].metadata },
tools: { ...catalog.tools, imageMetadata: imageMetadataLocales[language].toolName, imageToText: imageToTextLocales[language].toolName, categoryDescriptions: { ...catalog.tools.categoryDescriptions, privacy: privacyHubLocales[language].categoryDescription } },
categories: { ...catalog.categories, image: { ...catalog.categories.image, metadata: imageMetadataLocales[language].categoryDescription, toText: imageToTextLocales[language].categoryDescription } },
imageResize: imageResizeLocales[language].copy,
imageCompressor: imageCompressorLocales[language].copy,
imageMetadata: { ...imageMetadataLocales[language].copy, source: { ...imageMetadataLocales[language].copy.source, ...metadataUxLocales[language].image.source }, inspector: { ...imageMetadataLocales[language].copy.inspector, ...metadataUxLocales[language].image.inspector }, clean: { ...imageMetadataLocales[language].copy.clean, ...metadataUxLocales[language].image.clean }, policy: metadataUxLocales[language].image.policy },
imageToText: imageToTextLocales[language].copy,
pdfMetadata: { ...catalog.pdfMetadata, source: { ...catalog.pdfMetadata.source, ...metadataUxLocales[language].pdf.source }, inspector: { ...catalog.pdfMetadata.inspector, ...metadataUxLocales[language].pdf.inspector }, actions: { ...catalog.pdfMetadata.actions, ...metadataUxLocales[language].pdf.actions }, custom: metadataUxLocales[language].pdf.custom, errors: { ...catalog.pdfMetadata.errors, ...metadataUxLocales[language].pdf.errors } },
privacyHub: privacyHubLocales[language].copy,
}]));
Expand Down
Loading
Loading