diff --git a/src/utils/cn.ts b/src/utils/cn.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/src/utils/cn.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/utils/steganography.ts b/src/utils/steganography.ts new file mode 100644 index 0000000..a1d7fc3 --- /dev/null +++ b/src/utils/steganography.ts @@ -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, + }; +}