From 08037288e4aec9375dea637a076b3734fe0faf76 Mon Sep 17 00:00:00 2001 From: Mohamed-Abdelhalim2 Date: Tue, 16 Jun 2026 22:48:03 +0000 Subject: [PATCH] =?UTF-8?q?=D8=AA=D9=81=D8=A7=D8=B5=D9=8A=D9=84=20=D8=A7?= =?UTF-8?q?=D9=84=D9=85=D8=B4=D8=B1=D9=88=D8=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mohamed-Abdelhalim2 --- src/App.tsx | 908 +++++++++++++++++++++++++++++++++++++++++++ src/cn.ts | 6 + src/index.css | 45 +++ src/main.tsx | 24 ++ src/steganography.ts | 237 +++++++++++ 5 files changed, 1220 insertions(+) create mode 100644 src/App.tsx create mode 100644 src/cn.ts create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/steganography.ts diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..a01a9ad --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,908 @@ +/** + * نقطة الإدخال وواجهة المستخدم (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 +
+
+ +