1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redaction Checker | Verify if your PDF is truly redacted</title>
<meta name="title" content="Redaction Checker | Verify if your PDF is truly redacted">
<meta name="description" content="Check if your PDF is safely redacted. Our local-only tool detects hidden text layers, metadata, and unflattened elements. 100% private.">
<meta name="keywords" content="verify pdf redaction, check pdf safety, hidden text detector, pdf metadata checker, secure redaction verification">
<meta name="author" content="Just Redact">
<link rel="canonical" href="https://just-redact.com/redaction-checker.html" />
<!-- SEO: Open Graph / Social -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://just-redact.com/redaction-checker.html">
<meta property="og:title" content="Is your PDF actually redacted? Find out now.">
<meta property="og:description" content="Hidden data often lurks beneath black boxes. Use our local checker to find searchable text and metadata. No uploads.">
<meta property="og:image" content="https://just-redact.com/social-preview.png">
<meta property="twitter:card" content="summary_large_image">
<!-- SEO: Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Redaction Checker",
"operatingSystem": "Web",
"applicationCategory": "SecurityApplication",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"featureList": [
"Hidden text layer detection",
"Metadata audit",
"Annotation verification",
"100% local processing"
],
"description": "A secure, browser-based utility to verify if a PDF document has been properly redacted and flattened."
}
</script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,600;0,700;1,400&family=Inter:wght@400;500;600&display=swap');
body {
font-family: 'Inter', sans-serif;
color: #1f2937;
background-color: #f9fafb;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4 {
font-family: 'Crimson Pro', serif;
}
.drop-zone--over {
border-color: #10b981 !important;
background-color: #ecfdf5 !important;
}
</style>
</head>
<body class="flex flex-col min-h-screen">
<!-- Header -->
<header class="max-w-5xl mx-auto w-full px-6 py-8 flex justify-between items-center">
<a href="index.html" class="text-xl font-bold tracking-tight text-gray-900 font-serif flex items-center gap-2">
Just Redact.
</a>
<div class="flex gap-4">
<a href="redact.html" class="text-sm font-medium text-white bg-gray-900 hover:bg-gray-800 px-4 py-2 rounded-lg transition">Securely Redact Now</a>
</div>
</header>
<main class="flex-grow max-w-4xl mx-auto w-full px-6 pb-24">
<section class="text-center mt-8 mb-12">
<h1 class="text-4xl md:text-5xl font-semibold text-gray-900 mb-4">
Is your PDF actually redacted?
</h1>
<p class="text-lg text-gray-600 max-w-2xl mx-auto">
Hidden data often lurks beneath black boxes. Use our local checker to find searchable text, metadata, and unflattened layers.
</p>
</section>
<!-- Upload Zone -->
<div id="drop-zone" class="border-2 border-dashed border-gray-300 rounded-2xl p-12 text-center transition-all bg-white cursor-pointer hover:border-gray-400">
<input type="file" id="file-input" class="hidden" accept="application/pdf">
<div class="flex flex-col items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="text-xl font-medium text-gray-900 mb-1">Drop your PDF here</p>
<p class="text-sm text-gray-500">or click to browse from your device</p>
<p class="mt-4 text-xs text-gray-400 uppercase tracking-widest font-semibold">100% Local • No Uploads</p>
</div>
</div>
<!-- Analysis State -->
<div id="loading-state" class="hidden mt-12 text-center">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-300 border-t-gray-900 mb-4"></div>
<p class="text-gray-600 font-medium">Analyzing document safety...</p>
</div>
<!-- Results Dashboard -->
<div id="results-dashboard" class="hidden mt-12 animate-in fade-in duration-500">
<div class="grid md:grid-cols-3 gap-8">
<!-- Verdict Card -->
<div class="md:col-span-1 bg-white p-8 rounded-2xl shadow-sm border border-gray-100 flex flex-col items-center justify-center text-center">
<h2 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-6">Security Verdict</h2>
<div id="verdict-icon" class="mb-4">
<!-- Icon injected by JS -->
</div>
<p id="verdict-text" class="text-3xl font-bold mb-2"></p>
<p id="health-score" class="text-sm text-gray-500"></p>
</div>
<!-- Details Column -->
<div class="md:col-span-2 space-y-6">
<!-- Searchable Text -->
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-gray-900 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
</svg>
Searchable Text Layer
</h3>
<span id="text-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span>
</div>
<p id="text-description" class="text-sm text-gray-600 leading-relaxed">
Searching for hidden text that remains selectable or searchable even if covered by black boxes.
</p>
</div>
<!-- Metadata -->
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-gray-900 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Document Metadata
</h3>
<span id="metadata-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span>
</div>
<div id="metadata-list" class="space-y-2 mb-4 hidden">
<!-- Table injected by JS -->
</div>
<p id="metadata-description" class="text-sm text-gray-600 leading-relaxed">
Checking for hidden properties like Author, Producer, or Tool used that can leak identity or workflow details.
</p>
</div>
<!-- Annotations -->
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-gray-900 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
Unflattened Annotations
</h3>
<span id="annotations-badge" class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider"></span>
</div>
<p id="annotations-description" class="text-sm text-gray-600 leading-relaxed">
Detecting shapes, highlights, or comments that are "objects" on top of text rather than being part of the image.
</p>
</div>
</div>
</div>
<!-- Call to Action -->
<div id="cta-unsafe" class="mt-12 p-8 bg-gray-900 rounded-3xl text-center text-white hidden">
<h3 class="text-2xl font-bold mb-4 font-serif">Hidden data detected. Fix it now.</h3>
<p class="text-gray-400 mb-8 max-w-xl mx-auto">
Your PDF isn't fully redacted. Use Just Redact to permanently flatten this document into a secure image.
</p>
<a href="redact.html" class="inline-block bg-green-600 hover:bg-green-700 text-white font-bold py-4 px-10 rounded-full shadow-lg transition-transform transform hover:-translate-y-0.5">
Launch Secure Redactor
</a>
</div>
<div id="cta-safe" class="mt-12 p-8 bg-green-50 border border-green-100 rounded-3xl text-center hidden">
<h3 class="text-2xl font-bold text-green-900 mb-4 font-serif">This document is secure.</h3>
<p class="text-green-700 mb-0">
No text layers or hidden metadata were found. This PDF appears to be properly flattened.
</p>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-white py-12 border-t border-gray-100">
<div class="max-w-4xl mx-auto px-6 text-center text-sm text-gray-500">
© 2026 Just Redact. All processing happens 100% in your browser.
</div>
</footer>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
const fileInput = document.getElementById('file-input');
const dropZone = document.getElementById('drop-zone');
const resultsDashboard = document.getElementById('results-dashboard');
const loadingState = document.getElementById('loading-state');
// UI Elements for Results
const verdictIcon = document.getElementById('verdict-icon');
const verdictText = document.getElementById('verdict-text');
const healthScore = document.getElementById('health-score');
const textBadge = document.getElementById('text-badge');
const textDescription = document.getElementById('text-description');
const metadataBadge = document.getElementById('metadata-badge');
const metadataList = document.getElementById('metadata-list');
const metadataDescription = document.getElementById('metadata-description');
const annotationsBadge = document.getElementById('annotations-badge');
const annotationsDescription = document.getElementById('annotations-description');
const ctaUnsafe = document.getElementById('cta-unsafe');
const ctaSafe = document.getElementById('cta-safe');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drop-zone--over');
});
['dragleave', 'dragend'].forEach(type => {
dropZone.addEventListener(type, () => {
dropZone.classList.remove('drop-zone--over');
});
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drop-zone--over');
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleFile(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (fileInput.files.length) {
handleFile(fileInput.files[0]);
}
});
async function handleFile(file) {
if (file.type !== 'application/pdf') {
alert('Please upload a PDF file.');
return;
}
// UI Transition
resultsDashboard.classList.add('hidden');
loadingState.classList.remove('hidden');
dropZone.classList.add('opacity-50', 'pointer-events-none');
const reader = new FileReader();
reader.onload = async function() {
const typedarray = new Uint8Array(this.result);
await analyzePDF(typedarray);
};
reader.readAsArrayBuffer(file);
}
async function analyzePDF(data) {
try {
const pdfDoc = await pdfjsLib.getDocument(data).promise;
let findings = {
wordCount: 0,
annotationCount: 0,
metadata: {},
metadataFound: false
};
// 1. Metadata Check
const { info, metadata } = await pdfDoc.getMetadata();
const sensitiveKeys = ['Author', 'Creator', 'Producer', 'Title', 'Keywords'];
sensitiveKeys.forEach(key => {
if (info[key] && info[key].trim().length > 0) {
findings.metadata[key] = info[key];
findings.metadataFound = true;
}
});
// 2. Page Analysis
for (let i = 1; i <= pdfDoc.numPages; i++) {
const page = await pdfDoc.getPage(i);
// Text Layer
const textContent = await page.getTextContent();
textContent.items.forEach(item => {
if (item.str && item.str.trim().length > 0) {
findings.wordCount += item.str.trim().split(/\s+/).length;
}
});
// Annotations
const annotations = await page.getAnnotations();
findings.annotationCount += annotations.length;
}
renderResults(findings);
} catch (err) {
console.error("Analysis failed:", err);
alert("Could not analyze PDF. It might be password protected or corrupted.");
loadingState.classList.add('hidden');
dropZone.classList.remove('opacity-50', 'pointer-events-none');
}
}
function renderResults(findings) {
loadingState.classList.add('hidden');
resultsDashboard.classList.remove('hidden');
dropZone.classList.remove('opacity-50', 'pointer-events-none');
let score = 100;
if (findings.wordCount > 0) score -= 50;
// Only penalize for personally identifiable metadata
const piiKeys = ['Author', 'Title', 'Keywords'];
const hasPII = piiKeys.some(key => findings.metadata[key] && findings.metadata[key].trim().length > 0);
if (hasPII) score -= 20;
if (findings.annotationCount > 0) score -= 30;
// Limit score
score = Math.max(0, score);
const isSafe = score === 100;
// 1. Verdict UI
verdictText.innerText = isSafe ? 'Safe' : 'Unsafe';
verdictText.className = `text-3xl font-bold mb-2 ${isSafe ? 'text-green-600' : 'text-red-600'}`;
healthScore.innerText = `Redaction Health Score: ${score}/100`;
verdictIcon.innerHTML = isSafe
? `<div class="bg-green-100 p-4 rounded-full"><svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" /></svg></div>`
: `<div class="bg-red-100 p-4 rounded-full"><svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg></div>`;
// 2. Text Layer UI
if (findings.wordCount > 0) {
textBadge.innerText = 'Leaking';
textBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-red-100 text-red-700';
textDescription.innerHTML = `<strong>Danger:</strong> Found <span class="text-red-600 font-bold">${findings.wordCount}</span> searchable words. This document has not been flattened.`;
} else {
textBadge.innerText = 'Clean';
textBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700';
textDescription.innerText = 'No searchable text found. The document is likely flattened into an image.';
}
// 3. Metadata UI
if (findings.metadataFound) {
metadataBadge.innerText = 'Visible';
metadataBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-yellow-100 text-yellow-700';
metadataList.classList.remove('hidden');
let listHtml = `<table class="w-full text-sm text-left border-collapse">`;
for (const [key, value] of Object.entries(findings.metadata)) {
listHtml += `<tr class="border-b border-gray-50"><td class="py-2 font-medium text-gray-500 w-24">${key}</td><td class="py-2 text-gray-900 truncate max-w-xs">${value}</td></tr>`;
}
listHtml += `</table>`;
metadataList.innerHTML = listHtml;
metadataDescription.innerHTML = `<strong>Warning:</strong> Document properties contain potentially identifying information.`;
} else {
metadataBadge.innerText = 'Hidden';
metadataBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700';
metadataList.classList.add('hidden');
metadataDescription.innerText = 'No common sensitive metadata keys were found.';
}
// 4. Annotations UI
if (findings.annotationCount > 0) {
annotationsBadge.innerText = 'Found';
annotationsBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-yellow-100 text-yellow-700';
annotationsDescription.innerHTML = `<strong>Warning:</strong> <span class="font-bold">${findings.annotationCount}</span> unflattened objects found. These might be shapes or highlights used for redaction.`;
} else {
annotationsBadge.innerText = 'None';
annotationsBadge.className = 'px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-green-100 text-green-700';
annotationsDescription.innerText = 'No unflattened annotations or interactive elements detected.';
}
// 5. CTA Visibility
if (isSafe) {
ctaSafe.classList.remove('hidden');
ctaUnsafe.classList.add('hidden');
} else {
ctaSafe.classList.add('hidden');
ctaUnsafe.classList.remove('hidden');
}
}
</script>
</body>
</html>
|