feat: Add image uploader and metadata viewer components

- Implemented ImageUploader.svelte for drag-and-drop image uploads with metadata extraction.
- Created MetadataViewer.svelte to display extracted metadata in a user-friendly format.
- Added PrivacyInfo.svelte to inform users about client-side processing and privacy risks.
- Introduced ThemeToggle.svelte for dark mode functionality.
- Set up main.js for application entry point and configured Svelte with vite.
- Added Tailwind CSS for styling and configured Vite for asset management.
- Established svelte.config.js and tailwind.config.js for project settings.
This commit is contained in:
sharif committed 2025-06-23 22:23:22 +06:00
1 parent cf0d872dba
commit cbce104341
18 files changed
+5263

No files matched your search

+287
View File
@@ -0,0 +1,287 @@
<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 {
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;
ctx.clearRect(0, 0, canvas.width, canvas.height);
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();
}
}
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 from your image to protect your privacy. This
process happens entirely in your browser - no data is sent to any server.
</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>
</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>