Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
هذا الالتزام موجود في:
2026-06-17 00:04:37 +00:00
الأصل fda7df9ba2
التزام 8832e8ec22
2 ملفات معدلة مع 243 إضافات و0 حذوفات

6
src/utils/cn.ts Normal file
عرض الملف

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

237
src/utils/steganography.ts Normal file
عرض الملف

@@ -0,0 +1,237 @@
/**
* محرك الإخفاء الرقمي (Steganography Engine) - SteganoPixel
* ==========================================================
* الجزء الأول من الشرح الهيكلي:
* يعمل هذا الملف كمحرك أساسي لمعالجة الصور والتلاعب الدقيق في البتات (Bitwise Operations) على مستوى مصفوفة البكسلات (Uint8ClampedArray).
* يعتمد المنطق على تقنية Least Significant Bit (LSB)، حيث يتم استبدال البت الأقل أهمية في القنوات اللونية (أحمر، أخضر، أزرق) بالبتات المكونة للنص المراد إخفاؤه.
* هذا يضمن عدم تأثر الجودة البصرية للصورة الأصلية، حيث أن التغير في قيمة أي قناة لونية لا يتجاوز درجة واحدة (±1)، وهو ما تعجز العين البشرية عن تمييزه.
*
* الجزء الثاني من الشرح الهيكلي:
* يتفاعل هذا المحرك بشكل مباشر مع واجهة المستخدم في `App.tsx` من خلال ثلاث وظائف رئيسية: `embedTextInImageData` لتضمين النص،
* و `extractHiddenTextFromImageData` لقراءة النص وتفكيك التشفير، و `removeHiddenTextFromImageData` لتنظيف الصورة وتصفير البتات المستخدمة.
* يجب على المطورين مراعاة أن هذا المحرك يفترض أن مصفوفة البيانات (ImageData) غير مضغوطة ضغطاً ضياعياً (Lossless) أثناء دورة حياتها،
* لذا نستخدم دائماً صيغة PNG عند التصدير لضمان عدم ضياع البتات الدقيقة.
*/
const MAGIC_TEXT = "TXTIMG1";
const MAGIC_BYTES = new TextEncoder().encode(MAGIC_TEXT);
const LENGTH_BYTES = 4;
const HEADER_BYTES = MAGIC_BYTES.length + LENGTH_BYTES;
const TEXT_ENCODER = new TextEncoder();
const TEXT_DECODER = new TextDecoder();
export type HiddenTextMetadata = {
width: number;
height: number;
capacityBytes: number;
usedBytes: number;
messageBytes: number;
};
export type ExtractedHiddenText = HiddenTextMetadata & {
message: string;
};
/**
* حساب السعة الإجمالية للناقل (الصورة) بالبايت
*/
function getTotalCarrierBytes(width: number, height: number) {
// نستخدم 3 قنوات لونية (RGB) لكل بكسل، متجاهلين قناة الشفافية (Alpha) للحفاظ على المظهر.
return Math.floor((width * height * 3) / 8);
}
/**
* تحديد فهرس القناة اللونية الدقيق لمؤشر بت معين
*/
function channelIndexForBit(bitIndex: number) {
const pixelIndex = Math.floor(bitIndex / 3);
const channelOffset = bitIndex % 3;
// يتم تخزين ImageData بتنسيق RGBA، لذا نضرب في 4 للوصول للبكسل، ونضيف إزاحة القناة (0=R, 1=G, 2=B)
return pixelIndex * 4 + channelOffset;
}
/**
* تعيين البت الأقل أهمية (LSB) في قناة لونية محددة
*/
function setBit(data: Uint8ClampedArray, bitIndex: number, bit: number) {
const channelIndex = channelIndexForBit(bitIndex);
// 0xfe == 11111110 (تصفير البت الأخير ثم دمجه مع البت الجديد)
data[channelIndex] = (data[channelIndex] & 0xfe) | bit;
}
/**
* قراءة البت الأقل أهمية (LSB) من قناة لونية محددة
*/
function getBit(data: Uint8ClampedArray, bitIndex: number) {
return data[channelIndexForBit(bitIndex)] & 1;
}
/**
* كتابة مصفوفة من البايتات داخل البتات الأقل أهمية في الصورة
*/
function writeBytes(data: Uint8ClampedArray, startBit: number, bytes: Uint8Array) {
let bitCursor = startBit;
for (const byte of bytes) {
for (let shift = 7; shift >= 0; shift -= 1) {
setBit(data, bitCursor, (byte >> shift) & 1);
bitCursor += 1;
}
}
return bitCursor;
}
/**
* قراءة عدد محدد من البايتات من البتات الأقل أهمية في الصورة
*/
function readBytes(data: Uint8ClampedArray, startBit: number, byteCount: number) {
const bytes = new Uint8Array(byteCount);
let bitCursor = startBit;
for (let index = 0; index < byteCount; index += 1) {
let value = 0;
for (let bit = 0; bit < 8; bit += 1) {
value = (value << 1) | getBit(data, bitCursor);
bitCursor += 1;
}
bytes[index] = value;
}
return bytes;
}
/**
* التحقق من مطابقة مصفوفتين من البايتات (للتأكد من التوقيع السحري Magic Bytes)
*/
function bytesMatch(left: Uint8Array, right: Uint8Array) {
if (left.length !== right.length) return false;
for (let index = 0; index < left.length; index += 1) {
if (left[index] !== right[index]) return false;
}
return true;
}
/**
* تحويل طول الرسالة (رقم صحيح 32-بت) إلى 4 بايتات
*/
function lengthToBytes(length: number) {
const bytes = new Uint8Array(LENGTH_BYTES);
new DataView(bytes.buffer).setUint32(0, length);
return bytes;
}
/**
* تحويل 4 بايتات إلى رقم صحيح 32-بت يعبر عن طول الرسالة
*/
function bytesToLength(bytes: Uint8Array) {
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
}
/**
* استنساخ بيانات الصورة لضمان عدم التعديل المباشر على الأصل (Immutability)
*/
function cloneImageData(imageData: ImageData) {
return new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height);
}
/**
* حساب السعة التخزينية الصافية للصورة بالبايت (بعد استقطاع التوقيع والرأس)
*/
export function getImageCapacityBytes(width: number, height: number) {
return Math.max(0, getTotalCarrierBytes(width, height) - HEADER_BYTES);
}
/**
* تضمين نص سري داخل كائن ImageData وإرجاع النسخة المعدلة مع البيانات الوصفية
*/
export function embedTextInImageData(imageData: ImageData, text: string) {
const payload = TEXT_ENCODER.encode(text);
const capacityBytes = getImageCapacityBytes(imageData.width, imageData.height);
if (payload.length > capacityBytes) {
throw new Error("النص أطول من السعة المتاحة داخل هذه الصورة.");
}
const output = cloneImageData(imageData);
const packet = new Uint8Array(HEADER_BYTES + payload.length);
// بناء حزمة البيانات: [Magic Bytes] + [Payload Length (4 bytes)] + [Payload]
packet.set(MAGIC_BYTES, 0);
packet.set(lengthToBytes(payload.length), MAGIC_BYTES.length);
packet.set(payload, HEADER_BYTES);
writeBytes(output.data, 0, packet);
return {
imageData: output,
metadata: {
width: imageData.width,
height: imageData.height,
capacityBytes,
usedBytes: packet.length,
messageBytes: payload.length,
} satisfies HiddenTextMetadata,
};
}
/**
* فحص ImageData واستخراج النص السري المضمن مع البيانات الوصفية
*/
export function extractHiddenTextFromImageData(imageData: ImageData): ExtractedHiddenText {
const totalCarrierBytes = getTotalCarrierBytes(imageData.width, imageData.height);
if (totalCarrierBytes < HEADER_BYTES) {
throw new Error("أبعاد الصورة صغيرة جدًا ولا تحتوي على مساحة كافية للبيانات المخفية.");
}
// قراءة التوقيع السحري للتأكد من وجود بيانات مضمنة
const magic = readBytes(imageData.data, 0, MAGIC_BYTES.length);
if (!bytesMatch(magic, MAGIC_BYTES)) {
throw new Error("لم يتم العثور على نص مخفي داخل هذه الصورة.");
}
// قراءة طول الرسالة
const lengthBytes = readBytes(imageData.data, MAGIC_BYTES.length * 8, LENGTH_BYTES);
const messageBytes = bytesToLength(lengthBytes);
const capacityBytes = getImageCapacityBytes(imageData.width, imageData.height);
if (messageBytes > capacityBytes) {
throw new Error("البيانات المخفية داخل الصورة تبدو تالفة أو غير مكتملة.");
}
// قراءة جسم الرسالة
const payload = readBytes(imageData.data, HEADER_BYTES * 8, messageBytes);
return {
width: imageData.width,
height: imageData.height,
capacityBytes,
usedBytes: HEADER_BYTES + messageBytes,
messageBytes,
message: TEXT_DECODER.decode(payload),
};
}
/**
* تنظيف الصورة وإزالة أي نص مخفي منها عبر تصفير البتات المستخدمة
*/
export function removeHiddenTextFromImageData(imageData: ImageData) {
const extracted = extractHiddenTextFromImageData(imageData);
const output = cloneImageData(imageData);
const bitsToClear = extracted.usedBytes * 8;
// مسح البتات الأقل أهمية التي كانت تحمل البيانات
for (let bitIndex = 0; bitIndex < bitsToClear; bitIndex += 1) {
const channelIndex = channelIndexForBit(bitIndex);
output.data[channelIndex] = output.data[channelIndex] & 0xfe;
}
return {
imageData: output,
metadata: extracted,
};
}