Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
هذا الالتزام موجود في:
2026-06-17 00:03:54 +00:00
الأصل c90537c16b
التزام fda7df9ba2
3 ملفات معدلة مع 977 إضافات و0 حذوفات

908
src/App.tsx Normal file
عرض الملف

@@ -0,0 +1,908 @@
/**
* نقطة الإدخال وواجهة المستخدم (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>
);
}

45
src/index.css Normal file
عرض الملف

@@ -0,0 +1,45 @@
/**
* الأنماط العامة وإعدادات الخطوط (Global Styles & Typography) - SteganoPixel
* =========================================================================
* الجزء الأول من الشرح الهيكلي:
* يوفر هذا الملف التهيئة الأساسية للمشروع بالاعتماد على إطار عمل Tailwind CSS (عبر التوجيه @import "tailwindcss").
* نقوم بضبط المتغيرات الجذرية (:root) لتهيئة بيئة العرض التلقائية، وتحديد عائلة الخطوط الافتراضية (Inter و System-UI)
* لضمان عرض النصوص العربية والإنجليزية بأبهى حلة وبأقصى درجات الوضوح على جميع أنظمة التشغيل (Windows, macOS, iOS, Android).
*
* الجزء الثاني من الشرح الهيكلي:
* يعمل هذا الملف كطبقة تنسيق أساسية تمنع التفاوت البصري بين المتصفحات (Cross-Browser Normalization). نقوم بتصفير
* الهوامش الافتراضية (margin: 0)، وضبط السلوك التفاعلي للتمرير (scroll-behavior)، وتحديد الحد الأدنى لعرض الشاشة (320px)
* لضمان عدم انكسار التخطيط الشبكي (Grid/Flex) على الهواتف الذكية القديمة أو الشاشات الضيقة، مع دعم العرض التلقائي للصور.
*/
@import "tailwindcss";
:root {
color-scheme: light;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-width: 320px;
background: #f8fafc;
}
* {
box-sizing: border-box;
}
button,
input,
textarea {
font: inherit;
}
img {
display: block;
max-width: 100%;
}

24
src/main.tsx Normal file
عرض الملف

@@ -0,0 +1,24 @@
/**
* نقطة الإطلاق الجذرية (Application Bootstrapper) - SteganoPixel
* =================================================================
* الجزء الأول من الشرح الهيكلي:
* يقوم هذا الملف بدور المشغل الأساسي (Bootstrapper) لتطبيق React. يعتمد على أحدث واجهات البرمجة في React 18+ (`createRoot`)
* لتفعيل آلية التصيير المتزامن (Concurrent Rendering). كما يغلف شجرة المكونات بالكامل داخل مكون `<StrictMode>` الذي
* يساعد المطورين خلال بيئة التطوير على اكتشاف أي مشاكل خفية في دورة حياة المكونات، أو استخدام أدوات قديمة، أو آثار جانبية غير مرغوبة.
*
* الجزء الثاني من الشرح الهيكلي:
* يعمل هذا الملف كجسر يربط بين المستند الفعلي (`index.html`) ومنطق التطبيق المرئي (`App.tsx`). يقوم باستيراد ملف الأنماط
* الأساسي (`index.css`) لضمان حقن قواعد Tailwind CSS الجذرية قبل تصيير أي عنصر. إذا تعذر العثور على عنصر `#root` في
* المستند، سيتوقف المحرك فوراً، ما يمثل آلية حماية ضد تشغيل التطبيق في بيئة HTML غير متوافقة.
*/
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);