summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--index.html9
-rw-r--r--redact.html102
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 @@
<span class="text-green-600 mr-3 text-lg">✓</span>
<span><strong>Purely Web-Based.</strong> Works on any modern browser.</span>
</li>
+ <li class="flex items-start">
+ <span class="text-green-600 mr-3 text-lg">✓</span>
+ <span><strong>Search and Redact.</strong> Find and remove keywords instantly.</span>
+ </li>
</ul>
</div>
</div>
@@ -214,7 +219,7 @@
<div>
<h3 class="text-xl font-bold text-gray-900 mb-3">Redact with confidence</h3>
<p class="text-gray-600 text-lg leading-relaxed">
- Use the <strong>Draw</strong> tool to manually box out images or tables. Use the <strong>Select Text</strong> tool to highlight and redact exact sentences.
+ Use the <strong>Search</strong> tool to find and redact keywords instantly across the whole document. Or, use the <strong>Select Text</strong> and <strong>Draw</strong> tools for manual precision. Use whatever works best for the job.
</p>
</div>
</div>
@@ -250,7 +255,7 @@
</div>
</div>
<p class="mt-12 text-lg text-gray-600 max-w-2xl mx-auto leading-relaxed">
- 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.
</p>
</div>
</section>
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 () => {