ملف آخر
Signed-off-by: Mohamed-Abdelhalim2 <codepen.io.dropper985@passinbox.com>
هذا الالتزام موجود في:
6
src/utils/cn.ts
Normal file
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
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,
|
||||
};
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم