Files
Untrace/src/lib/ImageProcessor.svelte
T

386 lines
11 KiB
Svelte

<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
export let file;
export let disabled = false;
async function processImage() {
if (!file || disabled) return;
dispatch("processingStart");
try {
// Enhanced AI content credential detection before processing
await detectAndLogAICredentials();
const img = new Image();
img.onload = function () {
try {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = img.width;
canvas.height = img.height;
// Enhanced clearing for AI content credentials
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Create a new clean image without any metadata containers
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, 0, 0);
let outputType = "image/jpeg";
let quality = 0.92;
if (file.type === "image/png" && img.width * img.height < 1000000) {
outputType = "image/png";
quality = undefined;
}
canvas.toBlob(
(blob) => {
if (blob) {
verifyCleanImage(blob, file);
} else {
throw new Error("Failed to create blob from canvas");
}
},
outputType,
quality
);
} catch (error) {
console.error("Error processing with canvas:", error);
handleFallback();
}
};
img.onerror = function () {
console.error("Error loading image");
handleFallback();
};
img.src = URL.createObjectURL(file);
} catch (error) {
console.error("Error in processImage:", error);
handleFallback();
}
}
// Enhanced AI credential detection
async function detectAndLogAICredentials() {
try {
const { parse } = await import("exifr");
const fullMetadata = await parse(file, {
tiff: true,
exif: true,
gps: true,
xmp: true,
iptc: true,
icc: true,
jfif: true,
ihdr: true,
iptc: true,
photoshop: true,
silentErrors: true,
});
if (fullMetadata) {
console.log("🔍 Detected metadata before cleaning:", fullMetadata);
// Check for AI-specific metadata
const aiTags = [];
if (
fullMetadata.Software &&
typeof fullMetadata.Software === "string"
) {
const software = fullMetadata.Software.toLowerCase();
if (
software.includes("dall-e") ||
software.includes("dalle") ||
software.includes("chatgpt") ||
software.includes("openai") ||
software.includes("midjourney") ||
software.includes("stable diffusion")
) {
aiTags.push("AI Software Signature");
}
}
// Check for C2PA content credentials
if (
fullMetadata.Description &&
fullMetadata.Description.includes("c2pa")
) {
aiTags.push("C2PA Content Credentials");
}
if (
fullMetadata.Creator &&
(fullMetadata.Creator.includes("AI") ||
fullMetadata.Creator.includes("artificial intelligence"))
) {
aiTags.push("AI Creator Tags");
}
if (aiTags.length > 0) {
console.log("🤖 AI content credentials detected:", aiTags);
}
}
} catch (error) {
console.log("Could not analyze AI credentials:", error);
}
}
async function verifyCleanImage(cleanBlob, originalFile) {
try {
const { parse } = await import("exifr");
const cleanMetadata = await parse(cleanBlob, {
tiff: true,
exif: true,
gps: true,
xmp: true,
iptc: true,
icc: true,
silentErrors: true,
});
const hasRemainingMetadata =
cleanMetadata && Object.keys(cleanMetadata).length > 0;
if (hasRemainingMetadata) {
console.warn("Some metadata may still be present:", cleanMetadata);
// For extra security, try a second pass with different format
await performSecondPass(cleanBlob, originalFile);
} else {
console.log("✅ Image successfully cleaned - no metadata detected");
finalizeCleanImage(cleanBlob, originalFile);
}
} catch (error) {
console.log(
"Could not verify metadata removal (this is usually okay):",
error
);
// Proceed anyway - the canvas method should have worked
finalizeCleanImage(cleanBlob, originalFile);
}
}
// Second pass cleaning for stubborn metadata
async function performSecondPass(blob, originalFile) {
try {
console.log("Performing second pass metadata removal...");
const img = new Image();
img.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = img.width;
canvas.height = img.height;
// Use a different approach - explicitly clear and redraw
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
// Force JPEG output for maximum metadata removal
canvas.toBlob(
(finalBlob) => {
if (finalBlob) {
console.log("✅ Second pass completed");
finalizeCleanImage(finalBlob, originalFile, true);
} else {
finalizeCleanImage(blob, originalFile);
}
},
"image/jpeg",
0.95
);
};
img.onerror = () => finalizeCleanImage(blob, originalFile);
img.src = URL.createObjectURL(blob);
} catch (error) {
console.error("Second pass failed:", error);
finalizeCleanImage(blob, originalFile);
}
}
// Finalize the cleaned image
function finalizeCleanImage(blob, originalFile, wasSecondPass = false) {
const url = URL.createObjectURL(blob);
const originalExtension = originalFile.name.split(".").pop();
const nameWithoutExt = originalFile.name.replace(
new RegExp(`\\.${originalExtension}$`, "i"),
""
);
// Determine final extension based on blob type
let finalExtension = originalExtension;
if (blob.type === "image/jpeg") {
finalExtension = "jpg";
} else if (blob.type === "image/png") {
finalExtension = "png";
}
const filename = `${nameWithoutExt}-cleaned.${finalExtension}`;
dispatch("processingComplete", {
processedImage: {
url,
filename,
size: blob.size,
wasSecondPass,
originalSize: originalFile.size,
compressionRatio: (
((originalFile.size - blob.size) / originalFile.size) *
100
).toFixed(1),
},
});
}
function handleFallback() {
// Fallback: return original file
const url = URL.createObjectURL(file);
const fileExtension = file.name.split(".").pop();
const nameWithoutExt = file.name.replace(
new RegExp(`\\.${fileExtension}$`, "i"),
""
);
const filename = `${nameWithoutExt}-cleaned.${fileExtension}`;
dispatch("processingComplete", {
processedImage: {
url,
filename,
size: file.size,
},
});
}
</script>
<div class="processor-section">
<div class="text-center mb-6">
<p class="text-secondary mb-4">
Remove all EXIF metadata, AI content credentials, and provenance data from
your image to protect your privacy. This process happens entirely in your
browser - no data is sent to any server. Enhanced detection for C2PA
signatures, DALL-E watermarks, and other AI generation markers.
</p>
<button class="btn btn-primary btn-lg" on:click={processImage} {disabled}>
{#if disabled}
<svg
class="animate-spin"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M4 2a2 2 0 00-2 2v11a2 2 0 002 2h12a2 2 0 002-2V4a2 2 0 00-2-2H4zm0 2h12v11H4V4z"
clip-rule="evenodd"
/>
</svg>
Processing...
{:else}
<svg width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
clip-rule="evenodd"
/>
</svg>
Remove Metadata
{/if}
</button>
</div>
<div class="privacy-features">
<h4 class="font-semibold mb-3">What gets removed:</h4>
<div class="grid grid-2 gap-3 text-sm">
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
GPS location data
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
Camera make & model
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
Date & time taken
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
Camera settings
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
Software information
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
AI generation tags
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
C2PA content credentials
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
DALL-E/ChatGPT signatures
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
Midjourney watermarks
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
Stable Diffusion metadata
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
Adobe AI provenance
</div>
<div class="flex items-center gap-2">
<span class="text-success">✓</span>
JUMBF metadata containers
</div>
</div>
</div>
</div>
<style>
.text-success {
color: var(--success-color);
}
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.privacy-features {
background-color: var(--bg-secondary);
padding: 1.5rem;
border-radius: 0.5rem;
border: 1px solid var(--border-color);
}
</style>