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
39 changes: 38 additions & 1 deletion tests/browser/ocr-smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@ import { createOcrService } from "../../tools/shared/ocr.js";

const output = document.querySelector("#result");
const requestsBefore = performance.getEntriesByType("resource").map((entry) => entry.name);

async function verifyCategoryAvailability() {
const loadCategory = async (pathname) => {
const response = await fetch(pathname);
if (!response.ok) throw new Error(`${pathname} returned HTTP ${response.status}`);
return new DOMParser().parseFromString(await response.text(), "text/html");
};
const findOcrCard = (documentObject) => documentObject
.querySelector('[data-i18n="tools.imageToText"]')
?.closest("a.category-tool");
const cardPath = (card, categoryPath) => new URL(card.getAttribute("href"), new URL(categoryPath, location.origin)).pathname;

const [imageDocument, scanDocument] = await Promise.all([
loadCategory("/tools/image/"),
loadCategory("/tools/scan/"),
]);
const imageCard = findOcrCard(imageDocument);
const scanCard = findOcrCard(scanDocument);
if (!imageCard || !scanCard) throw new Error("Image to Text must be linked from both categories");
if (!imageCard.querySelector(".status--available") || !scanCard.querySelector(".status--available")) {
throw new Error("Image to Text must be available in both categories");
}
const imagePath = cardPath(imageCard, "/tools/image/");
const scanPath = cardPath(scanCard, "/tools/scan/");
if (imagePath !== "/tools/image/to-text/" || scanPath !== imagePath) {
throw new Error(`Category routes differ: ${imagePath}, ${scanPath}`);
}
const routeResponse = await fetch(scanPath);
if (!routeResponse.ok) throw new Error(`${scanPath} returned HTTP ${routeResponse.status}`);
const disabledControl = scanDocument.querySelector('[data-i18n="categories.scan.documentTitle"]')?.closest("article.category-tool");
if (!disabledControl?.querySelector('[data-i18n="tools.comingSoon"]')) {
throw new Error("Document Scanner must remain disabled");
}
return { imagePath, scanPath, disabledControl: true };
}

const canvas = document.createElement("canvas");
canvas.width = 720;
canvas.height = 220;
Expand All @@ -15,12 +51,13 @@ context.fillText("HELLO", 120, 145);
const image = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
const service = createOcrService();
try {
const categoryAvailability = await verifyCategoryAvailability();
const result = await service.recognizeImage(image, { language: "eng" });
const requests = performance.getEntriesByType("resource").map((entry) => entry.name).slice(requestsBefore.length);
const externalRequests = requests.filter((value) => new URL(value).origin !== location.origin);
if (!/HELLO/i.test(result.text)) throw new Error(`Unexpected OCR text: ${result.text}`);
if (externalRequests.length) throw new Error(`External requests: ${externalRequests.join(", ")}`);
window.__ocrSmokeResult = { ok: true, text: result.text.trim(), requests, externalRequests };
window.__ocrSmokeResult = { ok: true, text: result.text.trim(), requests, externalRequests, categoryAvailability };
output.textContent = `PASS: ${result.text.trim()}`;
} catch (error) {
window.__ocrSmokeResult = { ok: false, error: error.message, cause: error.cause?.message || null };
Expand Down
36 changes: 34 additions & 2 deletions tests/category-availability.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ function assertRoutesExist(categoryPage, routes) {
}
}

function linkedCard(list, titleKey) {
return [...list.matchAll(/<a class="category-tool surface" href="([^"]+)">([\s\S]*?)<\/a>/g)]
.find((match) => match[2].includes(`data-i18n="${titleKey}"`));
}

const imageHtml = read("tools/image/index.html");
const imageList = categoryList(imageHtml);
const imageRoutes = ["./converter/", "./resize/", "./compress/", "./metadata/", "./to-text/"];
Expand All @@ -35,6 +40,29 @@ assert.equal((imageList.match(/status--available/g) || []).length, 5);
assert.doesNotMatch(imageHtml, /<\/ul>\s*<li>/, "Image metadata card must remain inside the semantic list");
assertRoutesExist("tools/image/index.html", imageRoutes);

const imageOcrCard = linkedCard(imageList, "tools.imageToText");
assert.ok(imageOcrCard, "Image category must link Image to Text");
assert.match(imageOcrCard[2], /status--available/);
assert.doesNotMatch(imageOcrCard[2], /tools\.comingSoon|<article\b/);

const scanHtml = read("tools/scan/index.html");
const scanList = categoryList(scanHtml);
const scanOcrCard = linkedCard(scanList, "tools.imageToText");
assert.ok(scanOcrCard, "Scan and OCR category must link Image to Text");
assert.equal(scanOcrCard[1], "../image/to-text/");
assert.match(scanOcrCard[2], /status--available/);
assert.match(scanOcrCard[2], /data-i18n="categories\.image\.toText"/);
assert.doesNotMatch(scanOcrCard[2], /tools\.comingSoon|<article\b|categories\.scan\.ocr/);
assert.equal(
path.resolve(root, "tools/image", imageOcrCard[1]),
path.resolve(root, "tools/scan", scanOcrCard[1]),
"Image and Scan/OCR entries must resolve to the same route",
);
assertRoutesExist("tools/scan/index.html", [scanOcrCard[1]]);
assert.equal((scanList.match(/<article class="category-tool surface">/g) || []).length, 1);
assert.equal((scanList.match(/tools\.comingSoon/g) || []).length, 1);
assert.match(scanList, /data-i18n="categories\.scan\.documentTitle"/);

const privacyHtml = read("tools/privacy/index.html");
const privacyList = categoryList(privacyHtml);
const privacyRoutes = ["../image/metadata/", "../pdf/metadata/"];
Expand All @@ -50,13 +78,13 @@ const pdfList = categoryList(read("tools/pdf/index.html"));
assert.equal(linkedRoutes(pdfList).length, 6, "Every PDF production card must remain linked");
assert.equal((pdfList.match(/status--available/g) || []).length, 6);

for (const category of ["pdf", "image"]) {
for (const category of ["pdf", "image", "scan"]) {
const html = read(`tools/${category}/index.html`);
assert.match(html, /data-i18n="categories\.localNote">Available tools process file contents locally in browser memory\./);
assert.doesNotMatch(html, /Planned items are clearly marked/);
}

for (const category of ["scan", "media"]) {
for (const category of ["media"]) {
const plannedList = categoryList(read(`tools/${category}/index.html`));
assert.equal(linkedRoutes(plannedList).length, 0, `${category} must remain planned`);
assert.equal((plannedList.match(/<article class="category-tool surface">/g) || []).length, 2);
Expand All @@ -68,6 +96,10 @@ for (const [language, catalog] of Object.entries(translations)) {
assert.doesNotMatch(catalog.categories.localNote, /planned|geplant|prévu|planificad|準備中|준비 중/i, `${language} production category note mentions planned work`);
assert.equal(typeof catalog.privacyHub.imageDescription, "string", `${language} image scope is missing`);
assert.equal(typeof catalog.privacyHub.pdfDescription, "string", `${language} PDF scope is missing`);
assert.equal(typeof catalog.tools.imageToText, "string", `${language} Image to Text title is missing`);
assert.equal(typeof catalog.categories.image.toText, "string", `${language} Image to Text description is missing`);
assert.notEqual(catalog.tools.imageToText, "tools.imageToText", `${language} Image to Text title exposes a raw key`);
assert.notEqual(catalog.categories.image.toText, "categories.image.toText", `${language} Image to Text description exposes a raw key`);
}

console.log("Category availability, semantic list, production route, and planned-state checks passed.");
8 changes: 4 additions & 4 deletions tools/scan/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' blob: data:; connect-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'"><meta name="description" content="Local scanning and OCR tools planned for Secure Tools.">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' blob: data:; connect-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'"><meta name="description" content="Local scanning and OCR tools for private documents.">
<link rel="canonical" href="https://tools.securetools.app/tools/scan/">
<meta property="og:site_name" content="Secure Tools">
<meta property="og:url" content="https://tools.securetools.app/tools/scan/"><meta name="theme-color" content="#f7f7f5"><meta property="og:type" content="website"><meta property="og:title" content="Scan and OCR tools — Secure Tools"><meta property="og:description" content="Local scanning and OCR tools planned for Secure Tools.">
<meta property="og:url" content="https://tools.securetools.app/tools/scan/"><meta name="theme-color" content="#f7f7f5"><meta property="og:type" content="website"><meta property="og:title" content="Scan and OCR tools — Secure Tools"><meta property="og:description" content="Local scanning and OCR tools for private documents.">
<meta property="og:image" content="https://tools.securetools.app/assets/images/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Secure Tools — Local-first browser utilities for PDF, Image, and Privacy">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Scan and OCR tools — Secure Tools">
<meta name="twitter:description" content="Local scanning and OCR tools planned for Secure Tools.">
<meta name="twitter:description" content="Local scanning and OCR tools for private documents.">
<meta name="twitter:image" content="https://tools.securetools.app/assets/images/og-image.png">
<meta name="twitter:image:alt" content="Secure Tools — Local-first browser utilities for PDF, Image, and Privacy">
<title>Scan and OCR tools — Secure Tools</title><script src="../../js/theme-bootstrap.js"></script><link rel="stylesheet" href="../../css/base.css"><link rel="stylesheet" href="../../css/components.css"><link rel="stylesheet" href="../../css/pages.css"><link rel="stylesheet" href="../../css/categories.css"></head>
<body class="page-shell" data-page="scanCategory"><a class="skip-link" href="#main" data-i18n="common.skip">Skip to main content</a><header class="site-header"><div class="container header-inner"><a class="brand" href="../../" aria-label="Secure Tools home" data-i18n-aria-label="common.aria.home"><span class="brand-mark" aria-hidden="true"></span><span>Secure Tools</span></a><nav class="site-nav" aria-label="Primary navigation" data-i18n-aria-label="common.aria.primaryNavigation"><a href="../../#tools" data-i18n="common.nav.tools">Tools</a><a href="../../privacy/" data-i18n="common.nav.privacy">Privacy</a><a href="../../about/" data-i18n="common.nav.about">About</a><a href="https://github.com/SecureToolsProject/Secure_Tools" data-repository-link target="_blank" rel="noreferrer" data-i18n="common.nav.source">Source</a></nav><div class="header-controls"><label class="control-label"><span data-i18n="common.language">Language</span><select class="control" data-language-select><option value="en">English</option><option value="ko">한국어</option><option value="ja">日本語</option><option value="es">Español</option><option value="de">Deutsch</option><option value="fr">Français</option></select></label><label class="control-label"><span data-i18n="common.theme">Theme</span><select class="control" data-theme-select><option value="system" data-i18n="common.themes.system">System</option><option value="light" data-i18n="common.themes.light">Light</option><option value="dark" data-i18n="common.themes.dark">Dark</option></select></label></div></div></header>
<nav class="category-nav" aria-label="Tool categories" data-i18n-aria-label="common.aria.toolCategories"><div class="container category-nav__inner"><a href="../pdf/" data-i18n="categories.nav.pdf">PDF</a><a href="../image/" data-i18n="categories.nav.image">Image</a><a href="../privacy/" data-i18n="categories.nav.privacy">Privacy</a><a href="../scan/" aria-current="page" data-i18n="categories.nav.scan">Scan & OCR</a><a href="../media/" data-i18n="categories.nav.media">Media</a></div></nav>
<main id="main" class="page-main category-main"><div class="container"><section class="category-intro"><div><p class="eyebrow" data-i18n="categories.eyebrow">Tool category</p><h1 data-i18n="categories.scan.title">Scan & OCR tools</h1></div><p data-i18n="categories.scan.description">Turn scans into useful documents while keeping source material on your device.</p></section><ul class="category-tool-list"><li><article class="category-tool surface"><span class="status" data-i18n="tools.comingSoon">Coming soon</span><h2 data-i18n="categories.scan.ocrTitle">Image to text</h2><p data-i18n="categories.scan.ocr">Recognize text from an image locally.</p></article></li><li><article class="category-tool surface"><span class="status" data-i18n="tools.comingSoon">Coming soon</span><h2 data-i18n="categories.scan.documentTitle">Document scanner</h2><p data-i18n="categories.scan.document">Crop and clean photographed documents.</p></article></li></ul><p class="category-note" data-i18n="categories.plannedNote">This category is planned. No unavailable item is presented as working.</p></div></main><footer class="site-footer"><div class="container footer-inner"><div><a class="brand" href="../../" aria-label="Secure Tools home" data-i18n-aria-label="common.aria.home"><span class="brand-mark" aria-hidden="true"></span><span>Secure Tools</span></a><p data-i18n="common.footerStatement">Your files stay on your device.</p></div><nav aria-label="Footer navigation" data-i18n-aria-label="common.aria.footerNavigation"><a href="../../#tools" data-i18n="common.nav.tools">Tools</a><a href="../../privacy/" data-i18n="common.nav.privacy">Privacy</a><a href="../../about/" data-i18n="common.nav.about">About</a><a href="https://github.com/SecureToolsProject/Secure_Tools" data-repository-link target="_blank" rel="noreferrer" data-i18n="common.nav.source">Source</a></nav></div></footer><script type="module" src="../../js/main.js"></script></body></html>
<main id="main" class="page-main category-main"><div class="container"><section class="category-intro"><div><p class="eyebrow" data-i18n="categories.eyebrow">Tool category</p><h1 data-i18n="categories.scan.title">Scan & OCR tools</h1></div><p data-i18n="categories.scan.description">Turn scans into useful documents while keeping source material on your device.</p></section><ul class="category-tool-list"><li><a class="category-tool surface" href="../image/to-text/"><div class="category-tool__top"><span class="status status--available" data-i18n="tools.available">Available</span><span class="category-tool__arrow" aria-hidden="true">→</span></div><h2 data-i18n="tools.imageToText">Image to Text OCR</h2><p data-i18n="categories.image.toText">Extract editable text from an image with local English and Korean OCR.</p></a></li><li><article class="category-tool surface"><span class="status" data-i18n="tools.comingSoon">Coming soon</span><h2 data-i18n="categories.scan.documentTitle">Document scanner</h2><p data-i18n="categories.scan.document">Crop and clean photographed documents.</p></article></li></ul><p class="category-note" data-i18n="categories.localNote">Available tools process file contents locally in browser memory.</p></div></main><footer class="site-footer"><div class="container footer-inner"><div><a class="brand" href="../../" aria-label="Secure Tools home" data-i18n-aria-label="common.aria.home"><span class="brand-mark" aria-hidden="true"></span><span>Secure Tools</span></a><p data-i18n="common.footerStatement">Your files stay on your device.</p></div><nav aria-label="Footer navigation" data-i18n-aria-label="common.aria.footerNavigation"><a href="../../#tools" data-i18n="common.nav.tools">Tools</a><a href="../../privacy/" data-i18n="common.nav.privacy">Privacy</a><a href="../../about/" data-i18n="common.nav.about">About</a><a href="https://github.com/SecureToolsProject/Secure_Tools" data-repository-link target="_blank" rel="noreferrer" data-i18n="common.nav.source">Source</a></nav></div></footer><script type="module" src="../../js/main.js"></script></body></html>
Loading