/** * نقطة الإدخال وواجهة المستخدم (Main Application Component) - SteganoPixel * ========================================================================= * الجزء الأول من الشرح الهيكلي: * يمثل هذا الملف الواجهة التفاعلية الشاملة لتطبيق "SteganoPixel"، حيث يجمع بين طبقة العرض (React & Tailwind CSS) * وطبقة المعالجة المنطقية (Steganography Engine). يتيح المكون للمستخدمين التنقل بسلاسة بين نمطين أساسيين: نمط "الإخفاء" * لدمج النصوص السرية داخل الصور المرفوعة، ونمط "الاستخراج/التنظيف" لقراءة النصوص المضمنة مسبقاً أو تدميرها نهائياً. * يتم إدارة جميع الحالات التفاعلية (معاينة الصور، أخطاء الإدخال، تقدم العمليات، وحالة النسخ إلى الحافظة) محلياً داخل المتصفح. * * الجزء الثاني من الشرح الهيكلي: * يتفاعل المكون مع محرك الإخفاء الرقمي (`src/utils/steganography.ts`) من خلال تحويل ملفات الصور المرفوعة (File API) * إلى عناصر مرئية (HTMLImageElement) ثم رسمها على كائن `` في الذاكرة لاستخلاص مصفوفة البكسلات الخام (`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((resolve, reject) => { const image = new Image(); image.onload = () => resolve(image); image.onerror = () => reject(new Error("تعذر تحميل الصورة المحددة.")); image.src = src; }); } /** * إعداد وتحليل الصورة المحددة بواسطة المستخدم */ async function prepareSelectedImage(file: File): Promise { 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((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 (
{label}
{value}
); } /** * مكون رفع و اختيار الصور (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) => { const file = event.target.files?.[0]; if (file) onSelectFile(file); event.currentTarget.value = ""; }; const handleDrop = (event: DragEvent) => { event.preventDefault(); setIsDragging(false); const file = event.dataTransfer.files?.[0]; if (file) onSelectFile(file); }; return (

{title}

{description}

{selectedImage ? ( ) : null}
{selectedImage ? ( <> {selectedImage.file.name}
{selectedImage.file.name}
الأبعاد {selectedImage.width} × {selectedImage.height}
الحجم {formatBytes(selectedImage.file.size)}
النوع {selectedImage.file.type || "image/*"}
) : (
ستظهر هنا معاينة الصورة بعد اختيارها.
)}
); } /** * مكون عرض النتيجة وتنزيلها (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 (

{title}

{description}

{asset ? (
{asset.fileName}
{asset.fileName}
تم إنشاء نسخة PNG جديدة جاهزة للتنزيل والاستخدام.
{typeof asset.messageBytes === "number" ? ( ) : null} {metadata ? : null}
) : (
لا توجد نتيجة حتى الآن. عند تنفيذ العملية ستظهر هنا معاينة النسخة الجديدة مع زر التنزيل.
)}
); } export default function App() { const [mode, setMode] = useState("hide"); // حالات الإخفاء (Embedding states) const [encodeImage, setEncodeImage] = useState(null); const [encodeText, setEncodeText] = useState(""); const [encodeStatus, setEncodeStatus] = useState(""); const [encodeError, setEncodeError] = useState(""); const [encodeBusy, setEncodeBusy] = useState(false); const [encodedAsset, setEncodedAsset] = useState(null); const [encodedMetadata, setEncodedMetadata] = useState(null); // حالات الاستخراج والتنظيف (Extracting & Cleaning states) const [decodeImage, setDecodeImage] = useState(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(null); const [cleanedAsset, setCleanedAsset] = useState(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 (
S ستيجانو بكسل (SteganoPixel)

ضع النص داخل أي صورة واستخرجه أو أزله من نفس التطبيق

تطبيق ويب عربي متقدم لإخفاء النصوص السرية داخل الصور الرقمية بأسلوب غير ملحوظ بصرياً (LSB Steganography)، مع إمكانية قراءة النصوص المضمنة لاحقاً أو محوها بالكامل لتأمين خصوصيتك. تعمل جميع خوارزميات التشفير مباشرة داخل متصفحك دون إرسال أي ملفات إلى خوادم خارجية.

حفظ بصيغة PNG Lossless للحفاظ على البتات الدقيقة
يدعم التضمين · الاستخراج · إزالة الدمج (التنظيف)
خصوصية تامة 100% بدون اتصال بالإنترنت
{mode === "hide" ? (
{ void handleEncodeImageSelect(file); }} onClear={() => { setEncodeImage(null); setEncodedAsset(null); setEncodedMetadata(null); setEncodeStatus(""); setEncodeError(""); }} hint={ encodeImage?.file.type.includes("jpeg") ? "تنبيه: سيتم تحويل صورتك الـ JPG إلى PNG عند الحفظ لضمان عدم تلف البيانات المخفية بسبب ضغط الـ JPG الضياعي." : undefined } />

2) اكتب النص السري المراد إخفاؤه

تُشفّر الرسالة رقمياً وتُوزّع بانتظام على البتات الأقل أهمية للألوان (RGB)، ما يحافظ على النقاء البصري.

{messageBytes} / {capacityBytes || 0} bytes