From 7a18a37ebf3f389518d96dd6dba92774f04ee912 Mon Sep 17 00:00:00 2001 From: Tadas Vilkeliskis Date: Sun, 1 Feb 2026 12:36:09 -0500 Subject: search and redact --- index.html | 9 ++++-- redact.html | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 2a7d278..0c846d4 100644 --- a/index.html +++ b/index.html @@ -36,6 +36,7 @@ "featureList": [ "Local-only processing", "Permanent flattening", + "Search and redact", "Text selection redaction", "No file size limits" ], @@ -185,6 +186,10 @@ Purely Web-Based. Works on any modern browser. +
  • + + Search and Redact. Find and remove keywords instantly. +
  • @@ -214,7 +219,7 @@

    Redact with confidence

    - Use the Draw tool to manually box out images or tables. Use the Select Text tool to highlight and redact exact sentences. + Use the Search tool to find and redact keywords instantly across the whole document. Or, use the Select Text and Draw tools for manual precision. Use whatever works best for the job.

    @@ -250,7 +255,7 @@

    - Select text to redact it instantly, or draw boxes manually. When you export, we convert the page to a high-quality image, destroying the underlying data forever. + Search for keywords, select text, or draw boxes manually. When you export, we convert the page to a high-quality image, destroying the underlying data forever.

    diff --git a/redact.html b/redact.html index fb6df6a..ca74785 100644 --- a/redact.html +++ b/redact.html @@ -259,6 +259,16 @@ + +
    + +
    + + +
    @@ -661,6 +671,98 @@ } } + const searchBtn = document.getElementById('search-btn'); + const searchInput = document.getElementById('search-input'); + + searchBtn.addEventListener('click', async () => { + const term = searchInput.value.trim().toLowerCase(); + if (!term) return; + + const originalText = searchBtn.innerHTML; + searchBtn.disabled = true; + searchBtn.innerHTML = 'Searching...'; + + try { + let matchCount = 0; + for (let i = 1; i <= pdfDoc.numPages; i++) { + const page = await pdfDoc.getPage(i); + const textContent = await page.getTextContent(); + const viewport = page.getViewport({ scale: scale }); + + // Simple search: iterate items + // Improvement: This finds occurrences within a single text item. + // It does NOT yet handle phrases split across items. + for (const item of textContent.items) { + const text = item.str.toLowerCase(); + if (text.includes(term)) { + // Found a match (or multiple) in this item + // We redact the WHOLE item for now if it matches + // Ideally we'd measure width of substring, but that's complex without font metrics + + const tx = item.transform; + // PDF coordinates + // tx[4] = x, tx[5] = y (baseline) + // item.width = width + // item.height = height (sometimes 0, fallback to font size) + + let fontHeight = item.height; + if (!fontHeight) { + // Estimate from transform matrix + // Matrix: [sx, ky, kx, sy, tx, ty] + // Height is roughly sy (index 3) + fontHeight = Math.sqrt(tx[2]*tx[2] + tx[3]*tx[3]); + } + + // PDF Rect: [x_min, y_min, x_max, y_max] + // Note: PDF coords, (0,0) is bottom-left usually. + // y is baseline. Top of text is y + height. Bottom is y (roughly). + // Depending on font, descent might put it below y. + // Let's assume y is baseline and we want to cover up to ascent. + + const x = tx[4]; + const y = tx[5]; + const w = item.width; + const h = fontHeight; + + // PDF Rect for viewport conversion: [x1, y1, x2, y2] + // We need to pass [minX, minY, maxX, maxY] + const pdfRect = [x, y, x + w, y + h]; + + const rect = viewport.convertToViewportRectangle(pdfRect); + // rect is [x1, y1, x2, y2] in canvas coords + + // Normalize (x1 could be > x2 if flipped, though unlikely here) + const minX = Math.min(rect[0], rect[2]); + const minY = Math.min(rect[1], rect[3]); + const width = Math.abs(rect[0] - rect[2]); + const height = Math.abs(rect[1] - rect[3]); + + // Find the overlay for this page + const wrapper = document.querySelector(`.page-wrapper[data-page-index="${i}"]`); + if (wrapper) { + const overlay = wrapper.querySelector('.page-overlay'); + createRedactionBox(overlay, i, minX, minY, width, height); + matchCount++; + } + } + } + } + + if (matchCount === 0) { + alert('No matches found.'); + } else { + alert(`Redacted ${matchCount} occurrence(s).`); + } + + } catch (err) { + console.error('Search error:', err); + alert('An error occurred during search.'); + } finally { + searchBtn.disabled = false; + searchBtn.innerHTML = originalText; + } + }); + // --- Export Logic --- exportBtn.addEventListener('click', async () => { -- cgit v1.2.3