diff options
| author | Tadas Vilkeliskis <vilkeliskis.t@gmail.com> | 2026-02-01 12:36:09 -0500 |
|---|---|---|
| committer | Tadas Vilkeliskis <vilkeliskis.t@gmail.com> | 2026-02-01 12:36:09 -0500 |
| commit | 7a18a37ebf3f389518d96dd6dba92774f04ee912 (patch) | |
| tree | 85b6160c7b4835d603f242b4fa6ea81106700fc6 /redact.html | |
| parent | 0cbf2c0e6add8a4159b8d231c0eef08ff1e78be3 (diff) | |
search and redact
Diffstat (limited to 'redact.html')
| -rw-r--r-- | redact.html | 102 |
1 files changed, 102 insertions, 0 deletions
diff --git a/redact.html b/redact.html index fb6df6a..ca74785 100644 --- a/redact.html +++ b/redact.html @@ -259,6 +259,16 @@ <input type="radio" id="mode-select" name="mode" value="select"> <label for="mode-select">📝 Select Text</label> </div> + + <div style="border-left: 1px solid #ccc; height: 30px; margin: 0 10px;"></div> + + <div style="display: flex; gap: 5px;"> + <input type="text" id="search-input" placeholder="Find text..." style="padding: 6px; border: 1px solid #ccc; border-radius: 4px;"> + <button id="search-btn"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg> + Find & Redact + </button> + </div> <span id="page-count"></span> @@ -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 () => { |
