Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
909 أسطر
42 KiB
TypeScript
909 أسطر
42 KiB
TypeScript
/**
|
||
* نقطة الإدخال وواجهة المستخدم (Main Application Component) - SteganoPixel
|
||
* =========================================================================
|
||
* الجزء الأول من الشرح الهيكلي:
|
||
* يمثل هذا الملف الواجهة التفاعلية الشاملة لتطبيق "SteganoPixel"، حيث يجمع بين طبقة العرض (React & Tailwind CSS)
|
||
* وطبقة المعالجة المنطقية (Steganography Engine). يتيح المكون للمستخدمين التنقل بسلاسة بين نمطين أساسيين: نمط "الإخفاء"
|
||
* لدمج النصوص السرية داخل الصور المرفوعة، ونمط "الاستخراج/التنظيف" لقراءة النصوص المضمنة مسبقاً أو تدميرها نهائياً.
|
||
* يتم إدارة جميع الحالات التفاعلية (معاينة الصور، أخطاء الإدخال، تقدم العمليات، وحالة النسخ إلى الحافظة) محلياً داخل المتصفح.
|
||
*
|
||
* الجزء الثاني من الشرح الهيكلي:
|
||
* يتفاعل المكون مع محرك الإخفاء الرقمي (`src/utils/steganography.ts`) من خلال تحويل ملفات الصور المرفوعة (File API)
|
||
* إلى عناصر مرئية (HTMLImageElement) ثم رسمها على كائن `<canvas>` في الذاكرة لاستخلاص مصفوفة البكسلات الخام (`ImageData`).
|
||
* يتم تمرير هذه المصفوفة إلى دوال التشفير أو فك التشفير، ثم يعاد تجميع النتيجة في ملف `Blob` بصيغة `image/png` وتوليد
|
||
* رابط محلي (`URL.createObjectURL`) لتمكين المستخدم من تنزيل الصورة الناتجة بأمان تام ودون أي اتصال بخوادم خارجية.
|
||
*/
|
||
|
||
import { useEffect, useMemo, useState, type ChangeEvent, type DragEvent } from "react";
|
||
import {
|
||
embedTextInImageData,
|
||
extractHiddenTextFromImageData,
|
||
getImageCapacityBytes,
|
||
removeHiddenTextFromImageData,
|
||
type ExtractedHiddenText,
|
||
type HiddenTextMetadata,
|
||
} from "./utils/steganography";
|
||
|
||
type Mode = "hide" | "extract";
|
||
|
||
type SelectedImage = {
|
||
file: File;
|
||
url: string;
|
||
width: number;
|
||
height: number;
|
||
};
|
||
|
||
type GeneratedAsset = {
|
||
url: string;
|
||
fileName: string;
|
||
size: number;
|
||
width: number;
|
||
height: number;
|
||
messageBytes?: number;
|
||
};
|
||
|
||
type UploaderProps = {
|
||
title: string;
|
||
description: string;
|
||
selectedImage: SelectedImage | null;
|
||
onSelectFile: (file: File) => void;
|
||
onClear: () => void;
|
||
hint?: string;
|
||
};
|
||
|
||
/**
|
||
* تنسيق حجم البايتات إلى نصوص مقروءة للبشر
|
||
*/
|
||
function formatBytes(bytes: number) {
|
||
if (bytes < 1024) return `${bytes} بايت`;
|
||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} كيلوبايت`;
|
||
return `${(bytes / (1024 * 1024)).toFixed(2)} ميجابايت`;
|
||
}
|
||
|
||
/**
|
||
* استخراج الاسم الأساسي للملف بدون الامتداد
|
||
*/
|
||
function baseName(fileName: string) {
|
||
return fileName.replace(/\.[^.]+$/, "") || "image";
|
||
}
|
||
|
||
/**
|
||
* إطلاق عملية التنزيل برمجياً لضمان توافقية جميع المتصفحات
|
||
*/
|
||
function triggerDownload(url: string, fileName: string) {
|
||
const link = document.createElement("a");
|
||
link.href = url;
|
||
link.download = fileName;
|
||
link.rel = "noopener";
|
||
link.style.display = "none";
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
}
|
||
|
||
/**
|
||
* تحميل الصورة ككائن HTMLImageElement لاستخلاص أبعادها بدقة
|
||
*/
|
||
function loadImage(src: string) {
|
||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||
const image = new Image();
|
||
image.onload = () => resolve(image);
|
||
image.onerror = () => reject(new Error("تعذر تحميل الصورة المحددة."));
|
||
image.src = src;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* إعداد وتحليل الصورة المحددة بواسطة المستخدم
|
||
*/
|
||
async function prepareSelectedImage(file: File): Promise<SelectedImage> {
|
||
if (!file.type.startsWith("image/")) {
|
||
throw new Error("الملف المحدد ليس صورة صالحة.");
|
||
}
|
||
|
||
const url = URL.createObjectURL(file);
|
||
|
||
try {
|
||
const image = await loadImage(url);
|
||
return {
|
||
file,
|
||
url,
|
||
width: image.naturalWidth,
|
||
height: image.naturalHeight,
|
||
};
|
||
} catch (error) {
|
||
URL.revokeObjectURL(url);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* إنشاء عنصر Canvas في الذاكرة لنقل بيانات الصورة إليه
|
||
*/
|
||
function createCanvasFromImage(image: HTMLImageElement) {
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = image.naturalWidth;
|
||
canvas.height = image.naturalHeight;
|
||
|
||
const context = canvas.getContext("2d");
|
||
|
||
if (!context) {
|
||
throw new Error("تعذر الوصول إلى محرك الرسم داخل المتصفح.");
|
||
}
|
||
|
||
context.drawImage(image, 0, 0);
|
||
return { canvas, context };
|
||
}
|
||
|
||
/**
|
||
* تحويل محتوى Canvas إلى Blob بصيغة PNG لضمان حفظ البتات بدون ضياع
|
||
*/
|
||
function canvasToPngBlob(canvas: HTMLCanvasElement) {
|
||
return new Promise<Blob>((resolve, reject) => {
|
||
canvas.toBlob(
|
||
(blob) => {
|
||
if (!blob) {
|
||
reject(new Error("تعذر إنشاء ملف الصورة النهائي."));
|
||
return;
|
||
}
|
||
resolve(blob);
|
||
},
|
||
"image/png",
|
||
1,
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* مكون عرض الإحصائيات المصغرة (Pill Component)
|
||
*/
|
||
function StatPill({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<div className="rounded-2xl border border-white/70 bg-white/80 px-4 py-3 shadow-sm shadow-slate-200/60 backdrop-blur">
|
||
<div className="text-xs font-medium text-slate-500">{label}</div>
|
||
<div className="mt-1 text-sm font-semibold text-slate-900" dir="ltr">
|
||
{value}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* مكون رفع و اختيار الصور (Drag and Drop Uploader)
|
||
*/
|
||
function UploadCard({ title, description, selectedImage, onSelectFile, onClear, hint }: UploaderProps) {
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const inputId = `${title.replace(/\s+/g, "-")}-input`;
|
||
|
||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||
const file = event.target.files?.[0];
|
||
if (file) onSelectFile(file);
|
||
event.currentTarget.value = "";
|
||
};
|
||
|
||
const handleDrop = (event: DragEvent<HTMLLabelElement>) => {
|
||
event.preventDefault();
|
||
setIsDragging(false);
|
||
|
||
const file = event.dataTransfer.files?.[0];
|
||
if (file) onSelectFile(file);
|
||
};
|
||
|
||
return (
|
||
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
|
||
<div className="flex items-start justify-between gap-3">
|
||
<div>
|
||
<h3 className="text-base font-semibold text-slate-900">{title}</h3>
|
||
<p className="mt-1 text-sm leading-6 text-slate-600">{description}</p>
|
||
</div>
|
||
{selectedImage ? (
|
||
<button
|
||
type="button"
|
||
onClick={onClear}
|
||
className="rounded-full border border-slate-200 px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-600"
|
||
>
|
||
إزالة الصورة
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_220px]">
|
||
<label
|
||
htmlFor={inputId}
|
||
onDragOver={(event) => {
|
||
event.preventDefault();
|
||
setIsDragging(true);
|
||
}}
|
||
onDragLeave={() => setIsDragging(false)}
|
||
onDrop={handleDrop}
|
||
className={`group flex min-h-[220px] cursor-pointer flex-col items-center justify-center rounded-3xl border-2 border-dashed px-6 py-8 text-center transition ${
|
||
isDragging
|
||
? "border-violet-400 bg-violet-50"
|
||
: "border-slate-200 bg-slate-50/80 hover:border-violet-300 hover:bg-violet-50/70"
|
||
}`}
|
||
>
|
||
<input id={inputId} type="file" accept="image/*" className="hidden" onChange={handleChange} />
|
||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-violet-500 to-indigo-600 text-white shadow-lg shadow-violet-200/80">
|
||
<svg viewBox="0 0 24 24" className="h-7 w-7 fill-none stroke-current" strokeWidth="1.8">
|
||
<path d="M12 16V5" strokeLinecap="round" />
|
||
<path d="m7 10 5-5 5 5" strokeLinecap="round" strokeLinejoin="round" />
|
||
<path d="M5 19h14" strokeLinecap="round" />
|
||
</svg>
|
||
</div>
|
||
<div className="mt-4 text-lg font-semibold text-slate-900">
|
||
{selectedImage ? "استبدال الصورة الحالية" : "اسحب الصورة هنا أو اضغط للاختيار"}
|
||
</div>
|
||
<div className="mt-2 max-w-md text-sm leading-6 text-slate-500">
|
||
يدعم التطبيق صور PNG وJPG وWEBP وغيرها. تتم المعالجة محليًا داخل المتصفح فقط.
|
||
</div>
|
||
{hint ? <div className="mt-3 text-xs font-medium text-violet-700">{hint}</div> : null}
|
||
</label>
|
||
|
||
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-slate-50/90">
|
||
{selectedImage ? (
|
||
<>
|
||
<img
|
||
src={selectedImage.url}
|
||
alt={selectedImage.file.name}
|
||
className="h-[220px] w-full bg-white object-contain"
|
||
/>
|
||
<div className="space-y-2 border-t border-slate-200 px-4 py-4 text-sm text-slate-600">
|
||
<div className="font-medium text-slate-800">{selectedImage.file.name}</div>
|
||
<div className="flex items-center justify-between gap-4">
|
||
<span>الأبعاد</span>
|
||
<span dir="ltr">
|
||
{selectedImage.width} × {selectedImage.height}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between gap-4">
|
||
<span>الحجم</span>
|
||
<span dir="ltr">{formatBytes(selectedImage.file.size)}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between gap-4">
|
||
<span>النوع</span>
|
||
<span dir="ltr">{selectedImage.file.type || "image/*"}</span>
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="flex h-full min-h-[220px] flex-col items-center justify-center gap-3 px-5 text-center text-sm text-slate-500">
|
||
<div className="rounded-2xl bg-white p-4 shadow-sm">
|
||
<svg viewBox="0 0 24 24" className="h-8 w-8 fill-none stroke-slate-400" strokeWidth="1.7">
|
||
<rect x="3" y="4" width="18" height="16" rx="2" />
|
||
<path d="m7 15 3-3 3 3 4-5 2 3" strokeLinecap="round" strokeLinejoin="round" />
|
||
<circle cx="9" cy="9" r="1.2" fill="currentColor" className="text-slate-400" />
|
||
</svg>
|
||
</div>
|
||
<div>ستظهر هنا معاينة الصورة بعد اختيارها.</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* مكون عرض النتيجة وتنزيلها (Download and Preview Card)
|
||
*/
|
||
function DownloadCard({
|
||
title,
|
||
description,
|
||
asset,
|
||
metadata,
|
||
buttonLabel,
|
||
onDownload,
|
||
}: {
|
||
title: string;
|
||
description: string;
|
||
asset: GeneratedAsset | null;
|
||
metadata?: HiddenTextMetadata | ExtractedHiddenText | null;
|
||
buttonLabel: string;
|
||
onDownload: () => void;
|
||
}) {
|
||
return (
|
||
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
|
||
<div>
|
||
<h3 className="text-base font-semibold text-slate-900">{title}</h3>
|
||
<p className="mt-1 text-sm leading-6 text-slate-600">{description}</p>
|
||
</div>
|
||
|
||
{asset ? (
|
||
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_260px]">
|
||
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-slate-50/80">
|
||
<img src={asset.url} alt={asset.fileName} className="max-h-[340px] w-full bg-white object-contain" />
|
||
</div>
|
||
<div className="space-y-4 rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||
<div>
|
||
<div className="text-sm font-semibold text-slate-900">{asset.fileName}</div>
|
||
<div className="mt-1 text-sm text-slate-500">تم إنشاء نسخة PNG جديدة جاهزة للتنزيل والاستخدام.</div>
|
||
</div>
|
||
|
||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
|
||
<StatPill label="الحجم النهائي" value={formatBytes(asset.size)} />
|
||
<StatPill label="الأبعاد" value={`${asset.width} × ${asset.height}`} />
|
||
{typeof asset.messageBytes === "number" ? (
|
||
<StatPill label="النص المضمّن" value={formatBytes(asset.messageBytes)} />
|
||
) : null}
|
||
{metadata ? <StatPill label="المساحة المستخدمة" value={formatBytes(metadata.usedBytes)} /> : null}
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={onDownload}
|
||
className="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white transition hover:bg-slate-800"
|
||
>
|
||
<svg viewBox="0 0 24 24" className="h-4 w-4 fill-none stroke-current" strokeWidth="1.8">
|
||
<path d="M12 4v11" strokeLinecap="round" />
|
||
<path d="m7 10 5 5 5-5" strokeLinecap="round" strokeLinejoin="round" />
|
||
<path d="M5 20h14" strokeLinecap="round" />
|
||
</svg>
|
||
{buttonLabel}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="mt-4 rounded-3xl border border-dashed border-slate-200 bg-slate-50/80 px-6 py-12 text-center text-sm leading-7 text-slate-500">
|
||
لا توجد نتيجة حتى الآن. عند تنفيذ العملية ستظهر هنا معاينة النسخة الجديدة مع زر التنزيل.
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function App() {
|
||
const [mode, setMode] = useState<Mode>("hide");
|
||
|
||
// حالات الإخفاء (Embedding states)
|
||
const [encodeImage, setEncodeImage] = useState<SelectedImage | null>(null);
|
||
const [encodeText, setEncodeText] = useState("");
|
||
const [encodeStatus, setEncodeStatus] = useState("");
|
||
const [encodeError, setEncodeError] = useState("");
|
||
const [encodeBusy, setEncodeBusy] = useState(false);
|
||
const [encodedAsset, setEncodedAsset] = useState<GeneratedAsset | null>(null);
|
||
const [encodedMetadata, setEncodedMetadata] = useState<HiddenTextMetadata | null>(null);
|
||
|
||
// حالات الاستخراج والتنظيف (Extracting & Cleaning states)
|
||
const [decodeImage, setDecodeImage] = useState<SelectedImage | null>(null);
|
||
const [decodeStatus, setDecodeStatus] = useState("");
|
||
const [decodeError, setDecodeError] = useState("");
|
||
const [decodeBusy, setDecodeBusy] = useState(false);
|
||
const [cleanBusy, setCleanBusy] = useState(false);
|
||
const [copied, setCopied] = useState(false);
|
||
const [extracted, setExtracted] = useState<ExtractedHiddenText | null>(null);
|
||
const [cleanedAsset, setCleanedAsset] = useState<GeneratedAsset | null>(null);
|
||
|
||
// تنظيف عناوين الذاكرة عند تغيير الصور
|
||
useEffect(() => {
|
||
return () => {
|
||
if (encodeImage?.url) URL.revokeObjectURL(encodeImage.url);
|
||
};
|
||
}, [encodeImage]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (decodeImage?.url) URL.revokeObjectURL(decodeImage.url);
|
||
};
|
||
}, [decodeImage]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (encodedAsset?.url) URL.revokeObjectURL(encodedAsset.url);
|
||
};
|
||
}, [encodedAsset]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (cleanedAsset?.url) URL.revokeObjectURL(cleanedAsset.url);
|
||
};
|
||
}, [cleanedAsset]);
|
||
|
||
// مؤقت إعادة تعيين حالة النسخ
|
||
useEffect(() => {
|
||
if (!copied) return;
|
||
const timeout = window.setTimeout(() => setCopied(false), 1800);
|
||
return () => window.clearTimeout(timeout);
|
||
}, [copied]);
|
||
|
||
const messageBytes = useMemo(() => new TextEncoder().encode(encodeText).length, [encodeText]);
|
||
const capacityBytes = encodeImage ? getImageCapacityBytes(encodeImage.width, encodeImage.height) : 0;
|
||
const usagePercent = capacityBytes > 0 ? Math.min(100, (messageBytes / capacityBytes) * 100) : 0;
|
||
|
||
const handleEncodeImageSelect = async (file: File) => {
|
||
setEncodeError("");
|
||
setEncodeStatus("");
|
||
setEncodedAsset(null);
|
||
setEncodedMetadata(null);
|
||
|
||
try {
|
||
const selected = await prepareSelectedImage(file);
|
||
setEncodeImage(selected);
|
||
} catch (error) {
|
||
setEncodeError(error instanceof Error ? error.message : "حدث خطأ أثناء تحميل الصورة.");
|
||
}
|
||
};
|
||
|
||
const handleDecodeImageSelect = async (file: File) => {
|
||
setDecodeError("");
|
||
setDecodeStatus("");
|
||
setExtracted(null);
|
||
setCleanedAsset(null);
|
||
setCopied(false);
|
||
|
||
try {
|
||
const selected = await prepareSelectedImage(file);
|
||
setDecodeImage(selected);
|
||
} catch (error) {
|
||
setDecodeError(error instanceof Error ? error.message : "حدث خطأ أثناء تحميل الصورة.");
|
||
}
|
||
};
|
||
|
||
const handleEncode = async () => {
|
||
if (!encodeImage) {
|
||
setEncodeError("اختر صورة أولًا قبل محاولة تضمين النص.");
|
||
return;
|
||
}
|
||
|
||
if (!encodeText) {
|
||
setEncodeError("أدخل النص الذي تريد تضمينه داخل الصورة.");
|
||
return;
|
||
}
|
||
|
||
setEncodeBusy(true);
|
||
setEncodeError("");
|
||
setEncodeStatus("جارٍ تضمين النص داخل الصورة...");
|
||
|
||
try {
|
||
const image = await loadImage(encodeImage.url);
|
||
const { canvas, context } = createCanvasFromImage(image);
|
||
const source = context.getImageData(0, 0, canvas.width, canvas.height);
|
||
const { imageData, metadata } = embedTextInImageData(source, encodeText);
|
||
|
||
context.putImageData(imageData, 0, 0);
|
||
const blob = await canvasToPngBlob(canvas);
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
setEncodedMetadata(metadata);
|
||
setEncodedAsset({
|
||
url,
|
||
fileName: `${baseName(encodeImage.file.name)}-hidden.png`,
|
||
size: blob.size,
|
||
width: canvas.width,
|
||
height: canvas.height,
|
||
messageBytes: metadata.messageBytes,
|
||
});
|
||
setEncodeStatus("تم إنشاء نسخة PNG جديدة تحمل النص المخفي بنجاح.");
|
||
} catch (error) {
|
||
setEncodeError(error instanceof Error ? error.message : "تعذر تضمين النص داخل الصورة.");
|
||
setEncodeStatus("");
|
||
} finally {
|
||
setEncodeBusy(false);
|
||
}
|
||
};
|
||
|
||
const handleExtract = async () => {
|
||
if (!decodeImage) {
|
||
setDecodeError("اختر صورة أولًا لاستخراج النص المخفي.");
|
||
return;
|
||
}
|
||
|
||
setDecodeBusy(true);
|
||
setDecodeError("");
|
||
setDecodeStatus("جارٍ فحص الصورة واستخراج النص...");
|
||
|
||
try {
|
||
const image = await loadImage(decodeImage.url);
|
||
const { canvas, context } = createCanvasFromImage(image);
|
||
const source = context.getImageData(0, 0, canvas.width, canvas.height);
|
||
const result = extractHiddenTextFromImageData(source);
|
||
|
||
setExtracted(result);
|
||
setDecodeStatus("تم العثور على النص المخفي واستخراجه بنجاح.");
|
||
} catch (error) {
|
||
setExtracted(null);
|
||
setDecodeError(error instanceof Error ? error.message : "تعذر استخراج النص من الصورة.");
|
||
setDecodeStatus("");
|
||
} finally {
|
||
setDecodeBusy(false);
|
||
}
|
||
};
|
||
|
||
const handleClean = async () => {
|
||
if (!decodeImage) {
|
||
setDecodeError("اختر صورة أولًا ثم قم بتنظيفها من النص المخفي.");
|
||
return;
|
||
}
|
||
|
||
setCleanBusy(true);
|
||
setDecodeError("");
|
||
setDecodeStatus("جارٍ إنشاء نسخة نظيفة من الصورة...");
|
||
|
||
try {
|
||
const image = await loadImage(decodeImage.url);
|
||
const { canvas, context } = createCanvasFromImage(image);
|
||
const source = context.getImageData(0, 0, canvas.width, canvas.height);
|
||
const { imageData, metadata } = removeHiddenTextFromImageData(source);
|
||
|
||
context.putImageData(imageData, 0, 0);
|
||
const blob = await canvasToPngBlob(canvas);
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
setExtracted(metadata);
|
||
setCleanedAsset({
|
||
url,
|
||
fileName: `${baseName(decodeImage.file.name)}-clean.png`,
|
||
size: blob.size,
|
||
width: canvas.width,
|
||
height: canvas.height,
|
||
});
|
||
setDecodeStatus("تم إنشاء نسخة نظيفة بدون النص المخفي. يمكنك تنزيلها الآن.");
|
||
} catch (error) {
|
||
setCleanedAsset(null);
|
||
setDecodeError(error instanceof Error ? error.message : "تعذر تنظيف الصورة.");
|
||
setDecodeStatus("");
|
||
} finally {
|
||
setCleanBusy(false);
|
||
}
|
||
};
|
||
|
||
const handleCopy = async () => {
|
||
if (!extracted?.message) return;
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(extracted.message);
|
||
setCopied(true);
|
||
} catch {
|
||
setDecodeError("تعذر نسخ النص تلقائيًا. يمكنك نسخه يدويًا من مربع النص.");
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div
|
||
dir="rtl"
|
||
className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(124,58,237,0.16),_transparent_34%),linear-gradient(180deg,_#f8fafc,_#eef2ff_45%,_#ffffff)] text-slate-900"
|
||
>
|
||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||
<header className="overflow-hidden rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-violet-100/80 backdrop-blur sm:p-8">
|
||
<div className="grid gap-8 lg:grid-cols-[1.3fr_0.9fr] lg:items-center">
|
||
<div>
|
||
<div className="inline-flex items-center gap-2.5 rounded-full border border-violet-200 bg-violet-50 px-4 py-2 text-sm font-semibold text-violet-700">
|
||
<span className="flex h-3 w-3 items-center justify-center rounded-full bg-violet-500 text-[8px] text-white">
|
||
S
|
||
</span>
|
||
<span>ستيجانو بكسل (SteganoPixel)</span>
|
||
</div>
|
||
<h1 className="mt-5 text-3xl font-black tracking-tight text-slate-950 sm:text-5xl">
|
||
ضع النص داخل أي صورة واستخرجه أو أزله من نفس التطبيق
|
||
</h1>
|
||
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-600 sm:text-lg">
|
||
تطبيق ويب عربي متقدم لإخفاء النصوص السرية داخل الصور الرقمية بأسلوب غير ملحوظ بصرياً (LSB Steganography)،
|
||
مع إمكانية قراءة النصوص المضمنة لاحقاً أو محوها بالكامل لتأمين خصوصيتك. تعمل جميع خوارزميات التشفير
|
||
مباشرة داخل متصفحك دون إرسال أي ملفات إلى خوادم خارجية.
|
||
</p>
|
||
|
||
<div className="mt-6 flex flex-wrap gap-3 text-sm text-slate-600">
|
||
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
|
||
حفظ بصيغة PNG Lossless للحفاظ على البتات الدقيقة
|
||
</div>
|
||
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
|
||
يدعم التضمين · الاستخراج · إزالة الدمج (التنظيف)
|
||
</div>
|
||
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
|
||
خصوصية تامة 100% بدون اتصال بالإنترنت
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-4 sm:grid-cols-2">
|
||
<StatPill label="خوارزمية الدمج" value="3-Channel LSB" />
|
||
<StatPill label="بيئة المعالجة" value="Client-Side Browser" />
|
||
<StatPill label="دعم التصدير" value="PNG TrueColor (Lossless)" />
|
||
<StatPill label="مستوى الأمان" value="Zero Server Uploads" />
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="mt-8 grid gap-6 xl:grid-cols-[1.55fr_0.85fr]">
|
||
<section className="rounded-[2rem] border border-white/70 bg-white/80 p-4 shadow-2xl shadow-slate-200/60 backdrop-blur sm:p-6">
|
||
<div className="flex flex-wrap gap-3 rounded-3xl bg-slate-100/80 p-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => setMode("hide")}
|
||
className={`flex-1 rounded-2xl px-4 py-3 text-sm font-semibold transition sm:flex-none sm:min-w-[180px] ${
|
||
mode === "hide"
|
||
? "bg-slate-900 text-white shadow-lg shadow-slate-300"
|
||
: "bg-transparent text-slate-600 hover:bg-white"
|
||
}`}
|
||
>
|
||
تضمين نص سري داخل صورة
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setMode("extract")}
|
||
className={`flex-1 rounded-2xl px-4 py-3 text-sm font-semibold transition sm:flex-none sm:min-w-[180px] ${
|
||
mode === "extract"
|
||
? "bg-slate-900 text-white shadow-lg shadow-slate-300"
|
||
: "bg-transparent text-slate-600 hover:bg-white"
|
||
}`}
|
||
>
|
||
استخراج النص أو تنظيف الصورة
|
||
</button>
|
||
</div>
|
||
|
||
{mode === "hide" ? (
|
||
<div className="mt-6 space-y-6">
|
||
<UploadCard
|
||
title="1) اختر الصورة الأصلية (الناقل)"
|
||
description="ارفع الصورة التي تريد دمج النص داخلها. سيقوم التطبيق بقراءة أبعادها وحساب السعة التخزينية المتاحة لتضمين رسالتك."
|
||
selectedImage={encodeImage}
|
||
onSelectFile={(file) => {
|
||
void handleEncodeImageSelect(file);
|
||
}}
|
||
onClear={() => {
|
||
setEncodeImage(null);
|
||
setEncodedAsset(null);
|
||
setEncodedMetadata(null);
|
||
setEncodeStatus("");
|
||
setEncodeError("");
|
||
}}
|
||
hint={
|
||
encodeImage?.file.type.includes("jpeg")
|
||
? "تنبيه: سيتم تحويل صورتك الـ JPG إلى PNG عند الحفظ لضمان عدم تلف البيانات المخفية بسبب ضغط الـ JPG الضياعي."
|
||
: undefined
|
||
}
|
||
/>
|
||
|
||
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
|
||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||
<div>
|
||
<h3 className="text-base font-semibold text-slate-900">2) اكتب النص السري المراد إخفاؤه</h3>
|
||
<p className="mt-1 text-sm leading-6 text-slate-600">
|
||
تُشفّر الرسالة رقمياً وتُوزّع بانتظام على البتات الأقل أهمية للألوان (RGB)، ما يحافظ على النقاء
|
||
البصري.
|
||
</p>
|
||
</div>
|
||
<div
|
||
className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-2 text-sm text-slate-600"
|
||
dir="ltr"
|
||
>
|
||
{messageBytes} / {capacityBytes || 0} bytes
|
||
</div>
|
||
</div>
|
||
|
||
<textarea
|
||
value={encodeText}
|
||
onChange={(event) => setEncodeText(event.target.value)}
|
||
placeholder="اكتب رسالتك السرية هنا..."
|
||
className="mt-4 min-h-[180px] w-full rounded-3xl border border-slate-200 bg-slate-50/80 px-4 py-4 text-sm leading-7 text-slate-800 outline-none transition placeholder:text-slate-400 focus:border-violet-400 focus:bg-white focus:ring-4 focus:ring-violet-100"
|
||
/>
|
||
|
||
<div className="mt-4 space-y-3">
|
||
<div className="flex items-center justify-between text-xs font-medium text-slate-500">
|
||
<span>نسبة استهلاك السعة التخزينية</span>
|
||
<span dir="ltr">{usagePercent.toFixed(1)}%</span>
|
||
</div>
|
||
<div className="h-3 overflow-hidden rounded-full bg-slate-100">
|
||
<div
|
||
className={`h-full rounded-full transition-all ${
|
||
messageBytes > capacityBytes && capacityBytes > 0
|
||
? "bg-rose-500"
|
||
: "bg-gradient-to-r from-violet-500 to-indigo-600"
|
||
}`}
|
||
style={{ width: `${Math.min(100, usagePercent)}%` }}
|
||
/>
|
||
</div>
|
||
<div className="grid gap-3 sm:grid-cols-3">
|
||
<StatPill label="السعة الكلية المتاحة" value={formatBytes(capacityBytes)} />
|
||
<StatPill label="حجم الرسالة الحالي" value={formatBytes(messageBytes)} />
|
||
<StatPill label="صيغة التصدير" value="PNG Lossless" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-5 flex flex-wrap gap-3">
|
||
<button
|
||
type="button"
|
||
disabled={encodeBusy}
|
||
onClick={() => {
|
||
void handleEncode();
|
||
}}
|
||
className="inline-flex items-center justify-center rounded-2xl bg-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-violet-200 transition hover:bg-violet-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||
>
|
||
{encodeBusy ? "جارٍ التشفير والدمج..." : "إنشاء الصورة المضمّنة"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setEncodeText("");
|
||
setEncodeStatus("");
|
||
setEncodeError("");
|
||
}}
|
||
className="inline-flex items-center justify-center rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50"
|
||
>
|
||
مسح النص
|
||
</button>
|
||
</div>
|
||
|
||
{encodeStatus ? (
|
||
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
|
||
{encodeStatus}
|
||
</div>
|
||
) : null}
|
||
{encodeError ? (
|
||
<div className="mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700">
|
||
{encodeError}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
<DownloadCard
|
||
title="3) النتيجة النهائية الجاهزة"
|
||
description="انقر أدناه لتنزيل نسختك الجديدة من الصورة. تحتوي هذه الصورة الآن على النص السري المضمن وبأعلى جودة بصرية."
|
||
asset={encodedAsset}
|
||
metadata={encodedMetadata}
|
||
buttonLabel="تنزيل الصورة المضمّنة (PNG)"
|
||
onDownload={() => {
|
||
if (encodedAsset) triggerDownload(encodedAsset.url, encodedAsset.fileName);
|
||
}}
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="mt-6 space-y-6">
|
||
<UploadCard
|
||
title="1) اختر الصورة المراد فحصها أو تنظيفها"
|
||
description="ارفع الصورة المشتبه باحتوائها على نص مخفي. يمكنك استخراج وقراءة محتواها، أو محو البيانات السرية لإنتاج نسخة نظيفة."
|
||
selectedImage={decodeImage}
|
||
onSelectFile={(file) => {
|
||
void handleDecodeImageSelect(file);
|
||
}}
|
||
onClear={() => {
|
||
setDecodeImage(null);
|
||
setExtracted(null);
|
||
setCleanedAsset(null);
|
||
setDecodeStatus("");
|
||
setDecodeError("");
|
||
setCopied(false);
|
||
}}
|
||
/>
|
||
|
||
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
|
||
<div className="flex flex-wrap gap-3">
|
||
<button
|
||
type="button"
|
||
disabled={decodeBusy}
|
||
onClick={() => {
|
||
void handleExtract();
|
||
}}
|
||
className="inline-flex items-center justify-center rounded-2xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60"
|
||
>
|
||
{decodeBusy ? "جارٍ الفحص والاستخراج..." : "استخراج وقراءة النص السري"}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
disabled={cleanBusy}
|
||
onClick={() => {
|
||
void handleClean();
|
||
}}
|
||
className="inline-flex items-center justify-center rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
|
||
>
|
||
{cleanBusy ? "جارٍ محو البيانات..." : "إنشاء نسخة نظيفة خالية من النصوص"}
|
||
</button>
|
||
</div>
|
||
|
||
{decodeStatus ? (
|
||
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
|
||
{decodeStatus}
|
||
</div>
|
||
) : null}
|
||
{decodeError ? (
|
||
<div className="mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700">
|
||
{decodeError}
|
||
</div>
|
||
) : null}
|
||
|
||
<div className="mt-5 rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||
<div>
|
||
<h3 className="text-base font-semibold text-slate-900">محتوى النص المستخرج</h3>
|
||
<p className="mt-1 text-sm leading-6 text-slate-600">
|
||
إذا تم العثور على حزمة بيانات صحيحة في الصورة، سيتم تفكيكها وعرضها هنا.
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
disabled={!extracted?.message}
|
||
onClick={() => {
|
||
void handleCopy();
|
||
}}
|
||
className="rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||
>
|
||
{copied ? "تم النسخ بنجاح" : "نسخ النص"}
|
||
</button>
|
||
</div>
|
||
|
||
<textarea
|
||
readOnly
|
||
value={extracted?.message ?? ""}
|
||
placeholder="بعد رفع الصورة والنقر على «استخراج وقراءة النص السري»، ستظهر الرسالة المستخرجة هنا إن وجدت."
|
||
className="mt-4 min-h-[180px] w-full rounded-3xl border border-slate-200 bg-white px-4 py-4 text-sm leading-7 text-slate-800 outline-none placeholder:text-slate-400"
|
||
/>
|
||
|
||
<div className="mt-4 grid gap-3 sm:grid-cols-3">
|
||
<StatPill label="حجم النص المستخرج" value={formatBytes(extracted?.messageBytes ?? 0)} />
|
||
<StatPill label="حجم الحزمة المشفرة" value={formatBytes(extracted?.usedBytes ?? 0)} />
|
||
<StatPill label="أبعاد الصورة" value={extracted ? `${extracted.width} × ${extracted.height}` : "—"} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<DownloadCard
|
||
title="2) النسخة النظيفة المُعقمة"
|
||
description="إذا قمت بتنظيف الصورة، يمكنك تنزيل النسخة الجديدة أدناه. تم تصفير جميع بتات البيانات المضمنة فيها لضمان عدم إمكانية استخراجها مجدداً."
|
||
asset={cleanedAsset}
|
||
metadata={extracted}
|
||
buttonLabel="تنزيل الصورة النظيفة (PNG)"
|
||
onDownload={() => {
|
||
if (cleanedAsset) triggerDownload(cleanedAsset.url, cleanedAsset.fileName);
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<aside className="space-y-6">
|
||
<div className="rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-slate-200/60 backdrop-blur">
|
||
<h2 className="text-xl font-bold text-slate-950">آلية عمل التطبيق (LSB Architecture)</h2>
|
||
<div className="mt-5 space-y-4">
|
||
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||
<div className="text-sm font-bold text-slate-900">1. التشفير والتضمين (Embedding)</div>
|
||
<p className="mt-2 text-sm leading-7 text-slate-600">
|
||
يحول التطبيق حروفك إلى بايتات رقمية، ويضيف إليها توقيعاً سحرياً (`TXTIMG1`) وطول الرسالة، ثم يضعها
|
||
بانتظام في البت الأقل أهمية (LSB) لقنوات الألوان (RGB) لكل بكسل، ما يحافظ على دقة الألوان الأصلية.
|
||
</p>
|
||
</div>
|
||
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||
<div className="text-sm font-bold text-slate-900">2. القراءة والاستخراج (Extracting)</div>
|
||
<p className="mt-2 text-sm leading-7 text-slate-600">
|
||
عند الفحص، يقرأ المحرك البكسلات بالترتيب باحثاً عن التوقيع السحري. إذا تطابق التوقيع، يقرأ طول
|
||
الرسالة ويستخرج البتات المتبقية ليعيد تكوين النص الأصلي بدقة متناهية.
|
||
</p>
|
||
</div>
|
||
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
|
||
<div className="text-sm font-bold text-slate-900">3. محو البيانات (Sanitization)</div>
|
||
<p className="mt-2 text-sm leading-7 text-slate-600">
|
||
أداة التنظيف تقوم بحساب البتات التي تم استخدامها في الصورة وتصفرها تماماً (`& 0xFE`)، ما يعيد
|
||
البكسلات لحالة عشوائية طبيعية ويمنع أي محاولة مستقبلية لاستعادة النص.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-violet-100/70 backdrop-blur">
|
||
<h2 className="text-xl font-bold text-slate-950">ضوابط الحفاظ على الجودة</h2>
|
||
<ul className="mt-4 space-y-3 text-sm leading-7 text-slate-600">
|
||
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
|
||
يتم تصدير الصور حصراً بصيغة <span className="font-semibold text-slate-900">PNG Lossless</span>.
|
||
حفظها كـ JPG أو إرسالها عبر الواتساب/تليجرام بوضعية الضغط العادي يؤدي إلى تدمير البتات المخفية.
|
||
</li>
|
||
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
|
||
كلما كانت دقة الصورة وأبعادها أكبر، زادت سعتها التخزينية لاحتواء نصوص سرية أطول ومقالات كاملة.
|
||
</li>
|
||
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
|
||
التلاعب اللوني لا يتجاوز 1/255 من قيمة كل لون، وهو تغيير مستحيل الإدراك بالعين البشرية المجردة.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="rounded-[2rem] border border-white/70 bg-slate-950 p-6 text-white shadow-2xl shadow-slate-300/60">
|
||
<h2 className="text-xl font-bold">نصيحة أمنية واحترافية</h2>
|
||
<p className="mt-3 text-sm leading-7 text-slate-300">
|
||
لتبادل الرسائل السرية بنجاح، أرسل الصورة النهائية كـ «ملف» (File / Document) في برامج المحادثة أو البريد
|
||
الإلكتروني، لضمان عدم قيام تلك المنصات بضغط الصورة وإتلاف البيانات.
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|