الملفات الأساسية #1

مفتوح
Mohamed-Abdelhalim2 يريد دمج 5 التزامات من develop في main
16 ملفات معدلة مع 4522 إضافات و2 حذوفات

122
.gitignore مباع Normal file
عرض الملف

@@ -0,0 +1,122 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output 'dist' directory
dist
dist-ssr
# Output 'build' directory (for generic builders)
build
# Next.js build output
.next
# Nuxt.js build output
.nuxt
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# public
# vuepress build output
.vuepress/dist
# serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Mac OS X
.DS_Store
*.DS_Store
.AppleDouble
.LSOverride
Icon
# Windows
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
*.stackdump
Desktop.ini
$RECYCLE.BIN/
# JetBrains / IntelliJ IDEs
.idea/
*.iml
*.iws
# VS Code IDE
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

109
CUSTOMIZATION_GUIDE.md Normal file
عرض الملف

@@ -0,0 +1,109 @@
# دليل التخصيص وإدارة الإضافات (Customization & Asset Guide)
يوفر هذا الدليل شرحاً تفصيلياً لجميع الإضافات (Assets)، الخطوط، الألوان، والمكتبات المستخدمة في **SteganoPixel**، مع تعليمات خطوة بخطوة للمطورين حول كيفية تعديلها أو استبدالها وتصحيح المشاكل المحتملة.
---
## 1. الإضافات المستخدمة (Assets Inventory)
### أ. الصور والأيقونات (Images & Icons)
* **المسار الدقيق:** `public/` (للملفات الثابتة) وداخل كود `src/App.tsx` (أيقونات متجهات SVG المدمجة).
* **الغرض منها:** تعزيز التجربة البصرية التفاعلية (أيقونات السحب والإفلات، علامات النجاح، ومؤشرات التنزيل).
* **كيفية التعديل أو الاستبدال:**
* لاستبدال أيقونات الواجهة، ابحث عن وسوم `<svg>` في `src/App.tsx` واستبدلها بأي كود SVG مفضل (مثلاً من *Lucide* أو *Heroicons*).
* لإضافة شعار ثابت، ضع ملف الصورة (مثلاً `logo.png`) في مجلد `public/` واستدعه في الكود عبر `<img src="/logo.png" alt="Logo" />`.
* **تصحيح المشاكل:**
* **مشكلة:** الأيقونة تظهر بحجم ضخم أو مشوه.
* **الحل:** تأكد من تحديد خصائص العرض والطول في أصناف Tailwind (مثلاً `h-6 w-6`)، وتأكد من وجود خاصية `fill="none"` و `stroke="currentColor"` في وسم الـ SVG الأساسي.
### ب. الخطوط (Fonts & Typography)
* **المسار الدقيق:** معرّفة في `src/index.css` (`:root { font-family: ... }`) ويتم الاعتماد على الخطوط القياسية ومكتبة *Inter*.
* **الغرض منها:** تأمين قراءة ممتازة ومريحة للنصوص والتعليمات باللغتين العربية والإنجليزية.
* **كيفية تغيير الخطوط (عناوين، نصوص أساسية، أكواد):**
1. افتح ملف `src/index.css`.
2. لتغيير خط **العناوين والنصوص الأساسية**، قم باستيراد خطك المفضل من *Google Fonts* (مثلاً *Cairo* أو *Tajawal*) في رأس الملف:
```css
@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@400;700;900&display=swap');
:root {
font-family: 'Tajawal', system-ui, -apple-system, sans-serif;
}
```
3. لتخصيص خط **الأكواد والنصوص المستخرجة** (Monospace)، أضف تعريفاً خاصاً لعناصر `<textarea>`:
```css
textarea {
font-family: 'Fira Code', 'Courier New', monospace;
}
```
* **تصحيح المشاكل:**
* **مشكلة:** الخط العربي يظهر متقطعاً أو أوزانه (Bold/Regular) لا تعمل بشكل صحيح.
* **الحل:** تأكد من تضمين جميع الأوزان المطلوبة في رابط الاستيراد (`wght@400;600;800`)، وتأكد من عدم وجود أخطاء إملائية في اسم عائلة الخط داخل `:root`.
### جـ. الألوان والثيمات (Colors & Theming System)
* **المسار الدقيق:** معرّفة كفئات ذريّة (Utility Classes) في `src/App.tsx` و كمتغيرات في `src/index.css`.
* **الغرض منها:** تحديد الهوية البصرية للتطبيق (اللون البنفسجي، الخلفيات الزجاجية، والتنبيهات).
* **كيفية تعديل الألوان والثيمات:**
* **تغيير اللون الأساسي (Primary Theme):**
التطبيق يستخدم حالياً فئات `violet-500` حتى `violet-700` و `indigo-600`. لتحويل ثيم التطبيق إلى **الأزرق الملكي (Blue Theme)** مثلاً، استبدل كل:
* `from-violet-500 to-indigo-600` ➔ `from-blue-500 to-cyan-600`
* `bg-violet-600` hover `hover:bg-violet-700` ➔ `bg-blue-600` hover `hover:bg-blue-700`
* `text-violet-700` ➔ `text-blue-700`
* **تعديل خلفية التدرج العامة:**
افتح `src/App.tsx` وعدل حاوية الصفحة الجذرية:
```tsx
// من البنفسجي إلى الأبيض:
bg-[radial-gradient(circle_at_top,_rgba(124,58,237,0.16),_transparent_34%),linear-gradient(180deg,_#f8fafc,_#eef2ff_45%,_#ffffff)]
// لتحويله إلى ثيم رمادي داكن (Dark Theme):
bg-[radial-gradient(circle_at_top,_rgba(30,41,59,0.5),_transparent_34%),linear-gradient(180deg,_#0f172a,_#1e293b_45%,_#0f172a)] text-slate-100
```
* **تصحيح المشاكل:**
* **مشكلة:** التدرج اللوني لا يظهر أو يظهر كخلفية بيضاء صلبة.
* **الحل:** تأكد من عدم وجود مسافات فارغة غير مسموح بها داخل الأقواس المعقوفة في Tailwind `bg-[...]` (استخدم الشرطة السفلية `_` بدلاً من المسافة).
### د. المكتبات الخارجية (Third-Party Libraries)
* **المكتبات المستخدمة:**
1. `react` (v18.3.1): مكتبة الواجهات.
2. `react-dom` (v18.3.1): مشغل React على المتصفح.
3. `tailwindcss` (v4.0.0): إطار عمل الأنماط.
* **المسار الدقيق:** `package.json`.
* **كيفية التعديل أو التحديث:**
لتحديث المكتبات إلى أحدث إصدار، نفذ الأمر التالي في موجه الأوامر:
```bash
npm update
```
أو لتثبيت مكتبة إضافية (مثلاً `canvas-confetti` للاحتفال عند نجاح الاستخراج):
```bash
npm install canvas-confetti
npm install -D @types/canvas-confetti
```
* **تصحيح المشاكل:**
* **مشكلة:** ظهور تعارض في الحزم بعد التحديث (Peer Dependencies Conflict).
* **الحل:** احذف مجلد `node_modules` وملف `package-lock.json` ثم أعد التثبيت النظيف عبر `npm install`.
---
## 2. تخصيص المكونات التفاعلية (Components Customization)
### تخصيص بطاقة الرفع (`UploadCard`)
إذا أردت تغيير السلوك ليقبل ملفات PDF أو نصوص، يمكنك تعديل خاصية `accept` داخل مكون `UploadCard` في `src/App.tsx`:
```tsx
// الوضع الحالي (صور فقط):
<input id={inputId} type="file" accept="image/*" className="hidden" onChange={handleChange} />
// لتفعيل قبول الصور والوثائق:
<input id={inputId} type="file" accept="image/*,.pdf,.docx" className="hidden" onChange={handleChange} />
```
### تخصيص بطاقة التنزيل (`DownloadCard`)
لتعديل اسم الملف الافتراضي عند التنزيل، ابحث عن استدعاء `setEncodedAsset` في دالة `handleEncode`:
```tsx
setEncodedAsset({
url,
// يمكنك تغيير اللاحقة "-hidden" إلى أي عبارة أخرى:
fileName: `${baseName(encodeImage.file.name)}-secret-data.png`,
size: blob.size,
width: canvas.width,
height: canvas.height,
});
```

43
ERROR_LOG.md Normal file
عرض الملف

@@ -0,0 +1,43 @@
# سجل الأخطاء وتتبع المشاكل (Error Log & Bug Tracker)
يحتوي هذا الملف على تصنيف شامل لجميع الأخطاء البرمجية والمنطقية المحتملة أو التي تم اكتشافها في **SteganoPixel**، مع توفير السبب الجذري والحلول الفورية للمطورين.
---
## 1. جدول الأخطاء الشامل (Bugs Index)
| كود الخطأ | الوصف | السبب الجذري (Root Cause) | الملف / اللغة | الحل المقترح | الخطورة |
| :--- | :--- | :--- | :--- | :--- | :--- |
| **`ERR-STG-01`** | `النص أطول من السعة المتاحة داخل هذه الصورة.` | محاولة تضمين نص يتجاوز حجمه (بالبايت) عدد البتات المتاحة في القنوات اللونية (RGB) للصورة المرفوعة. | `steganography.ts` (Logic) | استخدام صورة ذات أبعاد أكبر (عرض × طول) أو تقليل طول النص المراد تضمينه. | **High** |
| **`ERR-STG-02`** | `أبعاد الصورة صغيرة جدًا ولا تحتوي على مساحة كافية.` | الصورة المرفوعة أصغر من أن تستوعب حتى التوقيع السحري (7 بايت) ورأس الطول (4 بايت). | `steganography.ts` (Logic) | فرض حد أدنى لأبعاد الصور المرفوعة (مثلاً 10×10 بكسل على الأقل). | **Medium** |
| **`ERR-STG-03`** | `لم يتم العثور على نص مخفي داخل هذه الصورة.` | عدم تطابق البايتات السبعة الأولى المستخرجة من الصورة مع التوقيع السحري `TXTIMG1`. | `steganography.ts` (Logic) | التأكد من أن الصورة المرفوعة تم تشفيرها مسبقاً باستخدام هذا التطبيق ولم تتعرض للضغط. | **Low** |
| **`ERR-STG-04`** | `البيانات المخفية داخل الصورة تبدو تالفة.` | طول الرسالة المقروء من الرأس يشير إلى حجم يلتهم مساحة تتجاوز الأبعاد الفعلية للصورة. | `steganography.ts` (Logic) | إبلاغ المستخدم بأن الصورة تعرضت لتعديل أو تلف أو تم قصها (Cropping). | **Medium** |
| **`ERR-UI-01`** | `عدم تنزيل الصورة عند الضغط على زر التنزيل.` | الاعتماد على وسم `<a>` مباشر مع رابط `Blob` داخل React، ما يمنع بعض المتصفحات من إطلاق التنزيل. | `App.tsx` (UI/UX) | إطلاق التنزيل برمجياً عبر حقن وسم `<a>` مؤقت في الـ DOM، النقر عليه، ثم حذفه. | **Critical** |
| **`ERR-UI-02`** | `فشل النسخ التلقائي للنص المستخرج إلى الحافظة.` | تقييد أمني من المتصفح (Clipboard API Protocol) يمنع النسخ إذا لم يتم عبر نقرة مباشرة، أو عدم استخدام `HTTPS`. | `App.tsx` (UI/UX) | توفير مربع نص مقروء (`<textarea>`) ليتمكن المستخدم من النسخ اليدوي، مع إظهار رسالة تنبيه. | **Low** |
| **`ERR-PERF-01`** | `تجميد المتصفح أثناء تشفير صورة عملاقة (4K+).` | تنفيذ حلقات المعالجة التكرارية لمليارات البتات على الخيط الرئيسي (Main UI Thread). | `steganography.ts` (Performance) | نقل دوال المعالجة (`embed` / `extract`) إلى خيط عمل خلفي (Web Worker) أو تقسيم المعالجة. | **High** |
| **`ERR-COMP-01`** | `تلف البيانات المخفية عند إرسال الصورة عبر الواتساب.` | قيام منصات التواصل الاجتماعي بإعادة ضغط الصور (Lossy JPG/WEBP Compression) ومحو البتات الدقيقة. | البيئة الخارجية (Compatibility) | توعية المستخدم بإرسال الصورة كـ «ملف / مستند» (File/Document) للحفاظ على بنيتها الخام. | **Critical** |
| **`ERR-SYN-01`** | `Property 'onDownload' is missing in type...` | عدم تمرير خاصية إجبارية معرّفة في واجهة TypeScript إلى المكون الفعلي عند استدعائه. | `App.tsx` (Syntax) | مراجعة تعريف واجهة `DownloadCard` وتمرير دالة `onDownload` في جميع الاستدعاءات. | **Critical** |
---
## 2. تصنيف الأخطاء حسب النوع (Categorized Diagnostics)
### أ. أخطاء بنيوية (Syntax Errors)
تحدث هذه الأخطاء أثناء مرحلة الكتابة والتجميع (Compilation). يمنع مترجم TypeScript تشغيل التطبيق إذا اكتشف عدم تطابق في الأنواع أو نسيان تمرير معاملات إجبارية (مثل `ERR-SYN-01`).
* **طريقة الوقاية:** الاعتماد الدائم على إضافات التحقق البرمجي (ESLint) وتركيب بيئة TypeScript صارمة (`"strict": true` في `tsconfig.json`).
### ب. أخطاء منطقية (Logic Errors)
تتركز في محرك الإخفاء الرقمي (`steganography.ts`). تتضمن حسابات إزاحة البتات (`Bitwise Shifts`) أو قراءة رؤوس غير متوافقة (مثل `ERR-STG-01` و `ERR-STG-03`).
* **طريقة الوقاية:** كتابة اختبارات وحدة (Unit Tests) للتحقق من أن نصاً معيناً يتم تضمينه ثم استخراجه بوضعية مطابقة 100% (`assert(extracted === original)`).
### جـ. أخطاء الأداء (Performance Errors)
تظهر عند التعامل مع صور ضخمة جداً (`4000×4000` بكسل فما فوق). مصفوفة `ImageData` لمثل هذه الصور تحتوي على أكثر من 64 مليون عنصر، والمرور عليها في الخيط الرئيسي يسبب تجميد الواجهة (`ERR-PERF-01`).
* **طريقة الوقاية:** إضافة شريط تقدم (Progress Bar) وتقسيم العمليات باستخدام `requestAnimationFrame` أو الاستعانة بـ `Web Workers`.
### د. أخطاء التوافق (Compatibility Errors)
تخرج عن سيطرة الكود المصدري وتتعلق بسلوك المتصفحات أو التطبيقات الخارجية. أشهرها قيام أنظمة التشغيل أو برامج المحادثة بتغيير صيغة الصورة من `PNG` إلى `JPG` (`ERR-COMP-01`).
* **طريقة الوقاية:** عرض إرشادات واضحة للمستخدمين ضمن الواجهة، وإجبار تصدير الصور بلاحقة `.png` صريحة.
### هـ. أخطاء الواجهة والتجربة (UI/UX Errors)
تؤثر على انسيابية العمل، مثل عدم استجابة زر التنزيل في متصفحات مدمجة داخل تطبيقات (In-App Browsers) أو فشل الصلاحيات الأمنية للنسخ (`ERR-UI-01` و `ERR-UI-02`).
* **طريقة الوقاية:** توفير بدائل يدوية دائماً (Fallback Mechanisms) مثل عرض الروابط المباشرة ومربعات النصوص القابلة للتحديد.

185
GIT.md Normal file
عرض الملف

@@ -0,0 +1,185 @@
# دليل إدارة المستودع وبروتوكولات العمل على GitHub (Git / GitHub Operations Guide)
يوفر هذا الملف إطار العمل الشامل والقياسي (Standard Operating Procedures) لإدارة مستودع **ستيجانو بكسل (SteganoPixel)** على منصة GitHub. يهدف هذا الدليل إلى توحيد جهود المطورين، تنظيم المساهمات البرمجية، وضمان أعلى مستويات الجودة والأمان خلال دورة حياة التطوير.
---
## 1. إعدادات المستودع الموصى بها على GitHub (Repository Configuration)
لضمان أمان الشفرة المصدرية وسلاسة العمل الجماعي، يُوصى بتطبيق الإعدادات التالية في لوحة تحكم المستودع (`Settings`):
### أ. البيانات الوصفية والهوية (Metadata & Topics)
* **اسم المستودع:** `steganopixel`
* **الوصف (Description):**
> SteganoPixel 🎨 - Advanced Client-Side LSB Steganography Web Application in Arabic for Embedding, Extracting, and Sanitizing Hidden Texts in Digital Images.
* **الوسوم (Topics / Tags):**
`steganography`, `lsb-steganography`, `react`, `tailwind-css`, `typescript`, `vite`, `image-processing`, `security`, `privacy`, `arabic-web-app`
* **الموقع الرسمي (Website):** رابط الاستضافة المباشر (مثل `https://steganopixel.vercel.app` أو `https://your-username.github.io/steganopixel`).
### ب. قواعد حماية الفروع (Branch Protection Rules)
يجب تفعيل حماية صارمة على الفرعين المحوريين `main` و `develop`:
1. **Require a pull request before merging:** منع الدفع المباشر (`Direct Push`) إلى هذه الفروع.
2. **Require approvals:** اشتراط موافقة مطور واحد على الأقل (`1 Approver`) قبل دمج أي Pull Request.
3. **Require status checks to pass before merging:** اشتراط اجتياز خط أنابيب التجميع والاختبار (`SteganoPixel CI/CD Pipeline`) بنجاح.
4. **Require conversation resolution:** اشتراط حل جميع الملاحظات والتعليقات البرمجية قبل الدمج.
5. **Do not allow bypassing the above settings:** تطبيق هذه القواعد على جميع المطورين، بما في ذلك مديري المستودع (Admins).
---
## 2. استراتيجية إدارة الفروع (Branching Strategy / GitFlow)
يعتمد المشروع على نموذج **GitFlow** المنظم. يتم تصنيف الفروع حسب الغرض منها لضمان عدم تداخل التعديلات:
```text
main ──*─────────────────────────────────────*── (Production Releases)
\ /
develop *──────*─────────────*────────────*──── (Staging / Integration)
\ / /
feature/* *─────────* / (New Features / Sub-tasks)
/
hotfix/* *──────── (Urgent Production Bug Fixes)
```
| نوع الفرع | التسمية القياسية | مشتق من | يُدمج إلى | الغرض |
| :--- | :--- | :--- | :--- | :--- |
| **الإنتاج (Production)** | `main` | — | — | يحتوي دائماً على الشفرة المستقرة والجاهزة للمستخدمين النهائيين. كل دمج هنا يقابله إصدار (Release). |
| **التطوير (Integration)** | `develop` | `main` | `main` | الفرع المحوري المجمع للميزات الجديدة. يُستخدم للاختبار الشامل قبل النقل للإنتاج. |
| **الميزات (Features)** | `feature/<name>` | `develop` | `develop` | لتطوير ميزة أو صفحة جديدة (مثلاً: `feature/confetti-animation` أو `feature/pdf-support`). |
| **الإصلاحات (Bug Fixes)** | `bugfix/<ticket>` | `develop` | `develop` | لإصلاح أخطاء غير حرجة تم اكتشافها أثناء التطوير (مثلاً: `bugfix/download-button-mobile`). |
| **الطوارئ (Hotfixes)** | `hotfix/<ticket>`| `main` | `main` و `develop`| لإصلاح أخطاء حرجة جداً في الإنتاج بشكل فوري (مثلاً: `hotfix/lsb-overflow-crash`). |
---
## 3. اصطلاحات كتابة رسائل الالتزام (Conventional Commits)
يعتمد المشروع اصطلاحات الالتزام القياسية العالمية (**Conventional Commits**) لضمان قراءة تاريخ التعديلات بوضوح وتوليد سجل التغييرات (`CHANGELOG.md`) تلقائياً.
### الهيكل القياسي:
```text
<type>(<scope>): <subject>
<body>
```
### أنواع الالتزامات المعتمدة (`Types`):
| النوع | المعنى | مثال قياسي بالإنجليزية |
| :--- | :--- | :--- |
| **`feat`** | إضافة ميزة برمجية أو وظيفية جديدة | `feat(steganography): add web worker for asynchronous embedding` |
| **`fix`** | إصلاح خطأ برمجي أو بصري | `fix(ui): resolve blob download link issue on mobile browsers` |
| **`docs`** | تعديل أو إضافة ملفات التوثيق | `docs(customization): update font replacement instructions` |
| **`style`** | تعديلات شكلية لا تؤثر على المنطق (مسافات، فواصل) | `style(components): reformat UploadCard component syntax` |
| **`refactor`** | إعادة هيكلة الكود دون إضافة ميزة أو إصلاح خطأ | `refactor(engine): separate bit reading logic into utility fn` |
| **`perf`** | تحسينات ترفع من أداء وسرعة التطبيق | `perf(canvas): use requestAnimationFrame to prevent ui freeze` |
| **`test`** | إضافة اختبارات آلية جديدة أو تعديل الحالية | `test(engine): add exhaustive suite for LSB 3-channel encode` |
| **`build`** | تعديلات على أدوات التجميع أو الحزم (`npm`, `Vite`) | `build(vite): configure static asset inline limit` |
| **`ci`** | تعديلات على إعدادات GitHub Actions | `ci(workflow): set output artifact retention days to 7` |
### قواعد هامة لكتابة الالتزام:
1. يجب كتابة الالتزام باللغة **الإنجليزية** لضمان التوافق العالمي.
2. يجب أن يبدأ الموضوع فعل أمر بصيغة المضارع (مثال: `add` وليس `added` أو `adds`).
3. ألا يتجاوز السطر الأول `72` حرفاً.
---
## 4. بروتوكول طلبات الدمج والمراجعة (Pull Request & Code Review)
لتقديم مساهمة برمجية ناجحة، يُرجى الالتزام بالبروتوكول التالي عند إنشاء طلب دمج (**Pull Request**):
1. **تحديث الفرع المحلي:** تأكد من عمل `git pull origin develop` ودمج أحدث التغييرات في فرعك قبل إنشاء الـ PR.
2. **العنوان القياسي:** استخدم نفس صيغة الالتزامات (مثال: `feat(ui): implement success confetti animations`).
3. **استخدام قالب طلب الدمج:** انسخ واملأ القالب التالي في صندوق وصف الـ PR:
```markdown
### 🎯 ملخص التغييرات (Summary of Changes)
- [شرح مختصر لما تم إضافته أو إصلاحه في هذا الـ PR]
### 🔗 التذكرة المتصلة (Related Issue)
- يغلق تذكرة رقم: #123
### 📋 قائمة التحقق (Checklist)
- [ ] تم اختبار التغييرات محلياً على أكثر من متصفح (Chrome, Firefox).
- [ ] الكود خالي من تعليقات الـ `console.log` والـ `TODO` المنسية.
- [ ] تم الالتزام بأفضل ممارسات الـ TypeScript (عدم استخدام `any`).
- [ ] تمت إضافة تعليقات توضيحية كاملة لأي جزء منطقي معقد.
### 🖼️ لقطات شاشة / معاينة (Screenshots / Before & After)
| قبل (Before) | بعد (After) |
| :---: | :---: |
| [ضع صورة هنا إن وجد] | [ضع صورة هنا إن وجد] |
```
---
## 5. إدارة الإصدارات والوسوم (Semantic Versioning - SemVer)
نتبع في **SteganoPixel** معيار التقسيم الدلالي للإصدارات (**SemVer: `MAJOR.MINOR.PATCH`**):
* **`MAJOR` (الإصدار الرئيسي):** تغييرات جذرية غير متوافقة مع الإصدارات السابقة (مثلاً: تغيير خوارزمية التشفير من LSB إلى خوارزمية DCT معمارية).
* **`MINOR` (الإصدار الفرعي):** إضافات وظائف جديدة متوافقة مع الإصدارات السابقة (مثلاً: إضافة خيار حماية النص بكلمة مرور).
* **`PATCH` (الإصدار التصحيحي):** إصلاحات أخطاء أو تحسينات أداء طفيفة (مثلاً: إصلاح مشكلة تنزيل الصورة في متصفح سفاري).
### كيفية إطلاق إصدار جديد (Releasing Protocol):
1. قم بإنشاء فرع إصدار مشتق من `develop` (مثال: `release/v1.1.0`).
2. حدّث رقم الإصدار في `package.json`.
3. ادمج الفرع إلى `main` وإلى `develop`.
4. قم بإنشاء وسم Git Tag على الفرع `main`:
```bash
git tag -a v1.1.0 -m "Release v1.1.0: Enhanced performance and new preview tools"
git push origin v1.1.0
```
5. توجّه إلى صفحة **Releases** في GitHub وأطلق الإصدار رسمياً مع إرفاق سجل التغييرات (`Changelog`).
---
## 6. قوالب الإبلاغ عن الأخطاء والميزات (GitHub Issue Templates)
لتسهيل إدارة المهام، يُنصح بتفعيل مجلد `.github/ISSUE_TEMPLATE` أو الاعتماد على القوالب التالية عند فتح `Issue` جديدة:
### أ. قالب الإبلاغ عن خطأ (Bug Report Template)
```markdown
**عنوان التذكرة:** [Bug]: وصف مختصر للخطأ
**وصف الخطأ:**
[اشرح الخطأ بالتفصيل وما الذي حدث خلافاً للمتوقع]
**خطوات إعادة التكرار (Steps to Reproduce):**
1. اذهب إلى صفحة '...'
2. ارفع صورة من نوع '...'
3. اضغط على زر '...'
4. شاهد الخطأ
**السلوك المتوقع (Expected Behavior):**
[ما الذي كان يفترض أن يحدث؟]
**البيئة (Environment):**
- نظام التشغيل: [مثال: Windows 11 / macOS 14]
- المتصفح وإصداره: [مثال: Chrome 122]
- حجم الصورة ونوعها: [مثال: 5MB PNG]
```
### ب. قالب طلب ميزة (Feature Request Template)
```markdown
**عنوان التذكرة:** [Feature]: اسم الميزة المقترحة
**المشكلة التي تحلها الميزة:**
[اشرح الصعوبة أو المشكلة الحالية التي تواجه المستخدمين]
**الحل المقترح (Proposed Solution):**
[اشرح كيف ستعمل الميزة الجديدة وكيف ستعزز من قدرات التطبيق]
**بدائل تم التفكير فيها (Alternatives Considered):**
[أي طرق أخرى فكرت فيها لحل المشكلة]
```
---
## 7. الأتمتة المدمجة (GitHub Actions CI/CD Pipeline)
يحتوي المستودع على خط أنابيب مؤتمت بالكامل معرّف في `.github/workflows/ci.yml`. يعمل هذا الخط تلقائياً عند أي عملية `push` أو `pull_request` على فروع التطوير والإنتاج لضمان الجودة.
### المهام التي ينفذها خط الأنابيب (CI Workflow):
1. **التحقق من التنسيق (Linting):** فحص الكود والتأكد من خلوه من الأخطاء البنيوية.
2. **فحص التنميط (Type Checking):** تشغيل مترجم `TypeScript` الصارم لضمان عدم وجود تناقض في الأنواع.
3. **تجميع الإنتاج (Production Build):** تنفيذ الأمر `npm run build` للتأكد من أن تطبيق `Vite` قادر على تحويل الكود بالكامل إلى مجلد `dist/` سليم.
4. **حفظ المخرجات (Artifacts):** حفظ المخرجات النهائية المجمعة لمدة 7 أيام ليتمكن المطورون من تنزيلها وفحصها.

135
README.md
عرض الملف

@@ -1,3 +1,134 @@
# non-destructive-text-image-editor # ستيجانو بكسل (SteganoPixel) - حبر الصور الرقمي
مشروع لبناء يوتيوب مصمم لمتابعة من تريد وما تحتاجه دون ملل وتصنيف الفيديوهات وجدولتها في أهدافك وخوارزمية معينة تصنيف الفيديوهات ![SteganoPixel Banner](https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=1200&q=80)
**تطبيق ويب عربي احترافي لإخفاء النصوص السرية داخل الصور الرقمية رقمياً (LSB Steganography)، واستخراجها أو إزالة دمجها بأمان تام داخل المتصفح.**
---
## 1. اسم المشروع والهوية البصرية
* **اسم المشروع المعتمد:** **ستيجانو بكسل (SteganoPixel)**. يعكس هذا الاسم بدقة دمج علم الإخفاء الرقمي (Steganography) مع أصغر وحدة بنائية في الصور الرقمية (Pixel).
* **الشعار / الأيقونة (Iconography Design System):**
* **الفلسفة البصرية:** تصميم يعتمد على دمج «قطرة الحبر السري» مع «مصفوفة البكسلات الرقمية»، ليعبر عن تضمين البيانات الخفية داخل الألوان.
* **الألوان الرئيسية:**
* **البنفسجي الملكي (Royal Violet):** `#7C3AED` (يعبر عن الغموض، الإبداع، والذكاء البرمجي).
* **النيلي الساطع (Bright Indigo):** `#4F46E5` (يضيف طابعاً تقنياً وعصرياً للتطبيق).
* **الألوان الثانوية (الخلفيات والتنبيهات):**
* **الأبيض النقي (Pure White):** `#FFFFFF` (للحاويات الشفافة الزجاجية Backdrop-Blur).
* **الأردوازي الداكن (Dark Slate):** `#0F172A` (للنصوص الأساسية والأزرار المحورية).
* **الأخضر الزمردي (Emerald Green):** `#10B981` (للدلالة على نجاح عمليات التشفير والاستخراج).
* **الأحمر الوردي (Rose Red):** `#F43F5E` (للتنبيهات والأخطاء وتجاوز السعة).
* **الشكل الهندسي والحواف:**
* يعتمد الشعار وحاويات التطبيق على هندسة الحواف فائقة النعومة (**Super-Rounded Corners / Squircle**) بنصف قطر يصل إلى `32px` (`rounded-[2rem]` و `rounded-3xl`)، ما يعطي راحة بصرية فائقة ويحاكي تصاميم أنظمة التشغيل الحديثة.
* **الرمز المستخدم:**
* الحرف **`S`** (بخط عريض استثنائي Black) مدمج داخل كبسولة بكسلية تتدرج ألوانها من البنفسجي إلى النيلي، مع إشعاع خلفي خفيف (Glow).
* **التدرجات اللونية (Gradients):**
* تدرج شعاعي شبكي في الخلفية العامة: `radial-gradient(circle at top, rgba(124,58,237,0.16), transparent 34%)` مدمج مع `linear-gradient(180deg, #f8fafc, #eef2ff 45%, #ffffff)`.
* **المواصفات التقنية للأيقونة:**
* **دقة متناهية (High-DPI Ready):** مصممة بالكامل باستخدام متجهات Vector SVG لتكون حادة وغير مبكسلة بجميع المقاسات من المقاس المجهري `16×16` وحتى المقاس العملاق `1024×1024`.
* **خفة الأداء:** لا تستهلك موارد وحدة المعالجة المركزية (CPU) أو معالج الرسوميات (GPU) بفضل اعتمادها على تأثيرات CSS Native سريعة التصيير.
* **الفهم الفوري:** بمجرد نظر المستخدم للأيقونة والواجهة، يدرك فوراً أن التطبيق يتعامل مع حماية وتشفير البيانات داخل ملفات الصور.
---
## 2. نظرة عامة على المشروع وأهدافه
يهدف **SteganoPixel** إلى توفير أداة أمنية وتشفيرية مجانية ومفتوحة المصدر للمستخدمين العرب، تتيح لهم تبادل المعلومات الحساسة أو إثبات ملكية الصور (Watermarking الرقمي) دون إثارة أي شكوك بصرية.
### الأهداف الرئيسية:
1. **الإخفاء البصري التام:** تضمين نصوص ضخمة داخل الصور دون أي تغيير يذكر في جودتها أو مظهرها العام.
2. **الخصوصية المطلقة (Zero-Server Architecture):** إجراء كافة عمليات المعالجة، التشفير، وفك التشفير داخل متصفح المستخدم (Client-Side) دون رفع أي بايت إلى خوادم خارجية.
3. **التعقيم وإزالة الدمج (Sanitization):** منح المستخدم القدرة على تدمير البيانات المضمنة في أي صورة وإنتاج نسخة «نظيفة» تماماً لمنع الأطراف الأخرى من قراءتها.
4. **تجربة استخدام استثنائية (UI/UX):** واجهة عربية جذابة وسريعة الاستجابة تعمل بكفاءة على الهواتف والأجهزة المكتبية بدون تثبيت.
---
## 3. لغات البرمجة المستخدمة وهندسة الترابط
تم بناء المشروع باستخدام أحدث تقنيات الويب القياسية المترابطة بإحكام:
* **TypeScript (TSX / TS):** اللغة الأساسية للمشروع. توفر تنميطاً صارماً (Strict Typing) يمنع الأخطاء البرمجية أثناء التطوير ويحكم ترابط واجهات React مع محرك الإخفاء.
* **React 18:** مكتبة بناء واجهات المستخدم. تدير الحالة التفاعلية (State Management) مثل مراحل رفع الصور، تقدم التشفير، وعرض النتائج الفورية.
* **Tailwind CSS v4:** إطار عمل الأنماط الذري. يُستخدم لبناء الهوية البصرية التجاوبية وتطبيق تأثيرات الزجاج (Glassmorphism) والتدرجات دون كتابة ملفات CSS مطولة.
* **HTML5 (Canvas & File API):** توفر البنية الأساسية للمستند وتقنيات قراءة الملفات المحلية، إضافة إلى محرك `<canvas>` الذي يسمح بالتلاعب المباشر في مصفوفة بكسلات الصور (`ImageData`).
### كيف تترابط هذه اللغات؟
1. يستقبل **HTML5** ملف الصورة من المستخدم عبر السحب والإفلات.
2. يلتقط **React** الملف ويحوله إلى كائن مرئي، بينما يُطبق **Tailwind CSS** أنماط التحميل التفاعلية.
3. يقوم كود **TypeScript** بنقل الصورة إلى عنصر `<canvas>` غير مرئي لقراءة البكسلات الخام (Red, Green, Blue).
4. يتدخل محرك الإخفاء لتعديل البتات، ثم يعيد المحتوى إلى **React** الذي يعرض زر التنزيل لملف الـ PNG الجديد.
---
## 4. هيكل المجلدات والملفات
```text
steganopixel/
├── public/ # الملفات العامة (الأيقونات والصور الثابتة)
├── src/ # الكود المصدري الأساسي
│ ├── utils/ # الأدوات والمحركات المستقلة
│ │ ├── steganography.ts # محرك التشفير والإخفاء وإزالة الدمج (LSB Engine)
│ │ └── cn.ts # أداة دمج وتوحيد أصناف Tailwind (إن وجدت)
│ ├── App.tsx # المكون التفاعلي الرئيسي (واجهة المستخدم الشاملة)
│ ├── main.tsx # نقطة إطلاق تطبيق React وربطه بالمستند
│ └── index.css # الأنماط الجذرية وإعدادات الخطوط
├── index.html # مستند الـ HTML الرئيسي وتعاريف الـ Meta/SEO
├── package.json # قائمة الاعتمادات (Dependencies) وسكربتات التشغيل
├── tsconfig.json # إعدادات مترجم TypeScript
└── vite.config.ts # إعدادات حاقن ومجمع الكود Vite
```
---
## 5. متطلبات التشغيل والتثبيت (Dependencies & Setup)
### المتطلبات الأساسية:
* **Node.js**: الإصدار 18.0.0 فما فوق.
* **NPM** أو **Yarn** أو **PNPM** لإدارة الحزم.
* متصفح ويب حديث يدعم `HTML5 Canvas` (Chrome, Firefox, Safari, Edge).
### الاعتمادات (Dependencies):
* `react` & `react-dom` (v18+): نواة التطبيق.
* `tailwindcss` (v4+): لتنسيق الواجهات.
* `@tailwindcss/vite`: إضافة التجميع السريع.
* `typescript`: للمترجم والتنميط.
### خطوات التثبيت والتشغيل المحلي:
1. **استنساخ أو تحميل المشروع:**
```bash
git clone https://github.com/your-username/steganopixel.git
cd steganopixel
```
2. **تثبيت الحزم والاعتمادات:**
```bash
npm install
```
3. **تشغيل خادم التطوير المحلي (Development Server):**
```bash
npm run dev
```
*سيفتح التطبيق تلقائياً في متصفحك على الرابط: `http://localhost:5173`*
4. **بناء المشروع للإنتاج (Production Build):**
```bash
npm run build
```
*ستجد الملفات النهائية المحسنة داخل مجلد `dist/` جاهزة للرفع على أي استضافة (Vercel, Netlify, GitHub Pages).*
---
## 6. التعليقات التوضيحية للأجزاء المتصلة (Architectural Commentary)
### محرك الإخفاء الرقمي (`src/utils/steganography.ts`)
يعمل هذا الجزء كمحرك أساسي لمعالجة الصور والتلاعب الدقيق في البتات (Bitwise Operations) على مستوى مصفوفة البكسلات (`Uint8ClampedArray`). يعتمد المنطق على تقنية Least Significant Bit (LSB)، حيث يتم استبدال البت الأقل أهمية في القنوات اللونية (أحمر، أخضر، أزرق) بالبتات المكونة للنص المراد إخفاؤه. هذا يضمن عدم تأثر الجودة البصرية للصورة الأصلية، حيث أن التغير في قيمة أي قناة لونية لا يتجاوز درجة واحدة (`±1`)، وهو ما تعجز العين البشرية عن تمييزه.
يتفاعل هذا المحرك بشكل مباشر مع واجهة المستخدم في `App.tsx` من خلال ثلاث وظائف رئيسية: `embedTextInImageData` لتضمين النص، و `extractHiddenTextFromImageData` لقراءة النص وتفكيك التشفير، و `removeHiddenTextFromImageData` لتنظيف الصورة وتصفير البتات المستخدمة. يجب على المطورين مراعاة أن هذا المحرك يفترض أن مصفوفة البيانات (`ImageData`) غير مضغوطة ضغطاً ضياعياً (Lossless) أثناء دورة حياتها، لذا نستخدم دائماً صيغة PNG عند التصدير لضمان عدم ضياع البتات الدقيقة.
### واجهة المستخدم الرئيسية (`src/App.tsx`)
يمثل هذا الجزء الواجهة التفاعلية الشاملة لتطبيق "SteganoPixel"، حيث يجمع بين طبقة العرض (`React` & `Tailwind CSS`) وطبقة المعالجة المنطقية (`Steganography Engine`). يتيح المكون للمستخدمين التنقل بسلاسة بين نمطين أساسيين: نمط "الإخفاء" لدمج النصوص السرية داخل الصور المرفوعة، ونمط "الاستخراج/التنظيف" لقراءة النصوص المضمنة مسبقاً أو تدميرها نهائياً. يتم إدارة جميع الحالات التفاعلية (معاينة الصور، أخطاء الإدخال، تقدم العمليات، وحالة النسخ إلى الحافظة) محلياً داخل المتصفح.
يتفاعل المكون مع محرك الإخفاء الرقمي من خلال تحويل ملفات الصور المرفوعة (`File API`) إلى عناصر مرئية (`HTMLImageElement`) ثم رسمها على كائن `<canvas>` في الذاكرة لاستخلاص مصفوفة البكسلات الخام (`ImageData`). يتم تمرير هذه المصفوفة إلى دوال التشفير أو فك التشفير، ثم يعاد تجميع النتيجة في ملف `Blob` بصيغة `image/png` وتوليد رابط محلي (`URL.createObjectURL`) لتمكين المستخدم من تنزيل الصورة الناتجة بأمان تام ودون أي اتصال بخوادم خارجية.

31
index.html Normal file
عرض الملف

@@ -0,0 +1,31 @@
<!doctype html>
<html lang="ar" dir="rtl">
<!--
ملف المستند الأساسي (Root HTML Document) - SteganoPixel
=======================================================
الجزء الأول من الشرح الهيكلي:
يمثل هذا الملف الهيكل العظمي لتطبيق الويب (Single Page Application). تم إعداده بشكل كامل لدعم اللغة العربية
(lang="ar" dir="rtl") من الألف إلى الياء. يحتوي على جميع وسوم البيانات الوصفية (Meta Tags) الضرورية لتحسين
محركات البحث (SEO) وضبط أبعاد العرض التجاوبي (Viewport) ليلائم جميع مقاسات الشاشات من الهواتف حتى الشاشات العملاقة.
الجزء الثاني من الشرح الهيكلي:
يستضيف هذا الملف الحاوية الجذرية (<div id="root"></div>) التي يقوم محرك React بتركيب شجرة المكونات التفاعلية
داخلها (عبر src/main.tsx). كما يربط حزم الجافاسكربت الحديثة بنظام الوحدات (type="module")، ما يتيح لأداة Vite
حقن التحديثات الفورية أثناء التطوير وتجميع الكود بأعلى كفاءة ممكنة عند التصدير للإنتاج.
-->
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="ستيجانو بكسل (SteganoPixel) - تطبيق ويب عربي متطور لإخفاء النصوص السرية داخل الصور الرقمية بأسلوب غير ملحوظ بصرياً واستخراجها أو تنظيفها بأمان تام داخل المتصفح."
/>
<meta name="keywords" content="إخفاء النص في الصورة, ستيجانوغرافي, تشفير النصوص, steganography, LSB, steganopixel" />
<meta name="author" content="SteganoPixel Application" />
<title>ستيجانو بكسل (SteganoPixel) - إخفاء واستخراج النصوص في الصور</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

2563
package-lock.json مولّد Normal file

تم حذف اختلاف الملف لأن الملف كبير جداً تحميل الاختلاف

28
package.json Normal file
عرض الملف

@@ -0,0 +1,28 @@
{
"name": "react-vite-tailwind",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"clsx": "2.1.1",
"react": "19.2.6",
"react-dom": "19.2.6",
"tailwind-merge": "3.4.0"
},
"devDependencies": {
"@tailwindcss/vite": "4.1.17",
"@types/node": "22.19.17",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "5.1.1",
"tailwindcss": "4.1.17",
"typescript": "5.9.3",
"vite": "7.3.2",
"vite-plugin-singlefile": "2.3.0"
}
}

38
src/.github/workflows/ci.yml مباع Normal file
عرض الملف

@@ -0,0 +1,38 @@
name: SteganoPixel CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
validate-and-build:
name: Lint, Type Check, and Build
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout Source Code
uses: actions/checkout@v4
- name: ⚙️ Setup Node.js Environment
uses: actions/setup-node@v4
with:
node-version: '18.x'
cache: 'npm'
- name: 📦 Install Dependencies
run: npm ci
- name: 🔍 Run TypeScript Type Checking
run: npm run build --if-present
- name: 🏗️ Verify Production Build
run: npm run build
- name: 📤 Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: steganopixel-production-dist
path: dist/
retention-days: 7

908
src/App.tsx Normal file
عرض الملف

@@ -0,0 +1,908 @@
/**
* نقطة الإدخال وواجهة المستخدم (Main Application Component) - SteganoPixel
* =========================================================================
* الجزء الأول من الشرح الهيكلي:
* يمثل هذا الملف الواجهة التفاعلية الشاملة لتطبيق "SteganoPixel"، حيث يجمع بين طبقة العرض (React & Tailwind CSS)
* وطبقة المعالجة المنطقية (Steganography Engine). يتيح المكون للمستخدمين التنقل بسلاسة بين نمطين أساسيين: نمط "الإخفاء"
* لدمج النصوص السرية داخل الصور المرفوعة، ونمط "الاستخراج/التنظيف" لقراءة النصوص المضمنة مسبقاً أو تدميرها نهائياً.
* يتم إدارة جميع الحالات التفاعلية (معاينة الصور، أخطاء الإدخال، تقدم العمليات، وحالة النسخ إلى الحافظة) محلياً داخل المتصفح.
*
* الجزء الثاني من الشرح الهيكلي:
* يتفاعل المكون مع محرك الإخفاء الرقمي (`src/utils/steganography.ts`) من خلال تحويل ملفات الصور المرفوعة (File API)
* إلى عناصر مرئية (HTMLImageElement) ثم رسمها على كائن `<canvas>` في الذاكرة لاستخلاص مصفوفة البكسلات الخام (`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<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error("تعذر تحميل الصورة المحددة."));
image.src = src;
});
}
/**
* إعداد وتحليل الصورة المحددة بواسطة المستخدم
*/
async function prepareSelectedImage(file: File): Promise<SelectedImage> {
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<Blob>((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 (
<div className="rounded-2xl border border-white/70 bg-white/80 px-4 py-3 shadow-sm shadow-slate-200/60 backdrop-blur">
<div className="text-xs font-medium text-slate-500">{label}</div>
<div className="mt-1 text-sm font-semibold text-slate-900" dir="ltr">
{value}
</div>
</div>
);
}
/**
* مكون رفع و اختيار الصور (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<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) onSelectFile(file);
event.currentTarget.value = "";
};
const handleDrop = (event: DragEvent<HTMLLabelElement>) => {
event.preventDefault();
setIsDragging(false);
const file = event.dataTransfer.files?.[0];
if (file) onSelectFile(file);
};
return (
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-slate-900">{title}</h3>
<p className="mt-1 text-sm leading-6 text-slate-600">{description}</p>
</div>
{selectedImage ? (
<button
type="button"
onClick={onClear}
className="rounded-full border border-slate-200 px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:border-rose-200 hover:text-rose-600"
>
إزالة الصورة
</button>
) : null}
</div>
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_220px]">
<label
htmlFor={inputId}
onDragOver={(event) => {
event.preventDefault();
setIsDragging(true);
}}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
className={`group flex min-h-[220px] cursor-pointer flex-col items-center justify-center rounded-3xl border-2 border-dashed px-6 py-8 text-center transition ${
isDragging
? "border-violet-400 bg-violet-50"
: "border-slate-200 bg-slate-50/80 hover:border-violet-300 hover:bg-violet-50/70"
}`}
>
<input id={inputId} type="file" accept="image/*" className="hidden" onChange={handleChange} />
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-violet-500 to-indigo-600 text-white shadow-lg shadow-violet-200/80">
<svg viewBox="0 0 24 24" className="h-7 w-7 fill-none stroke-current" strokeWidth="1.8">
<path d="M12 16V5" strokeLinecap="round" />
<path d="m7 10 5-5 5 5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 19h14" strokeLinecap="round" />
</svg>
</div>
<div className="mt-4 text-lg font-semibold text-slate-900">
{selectedImage ? "استبدال الصورة الحالية" : "اسحب الصورة هنا أو اضغط للاختيار"}
</div>
<div className="mt-2 max-w-md text-sm leading-6 text-slate-500">
يدعم التطبيق صور PNG وJPG وWEBP وغيرها. تتم المعالجة محليًا داخل المتصفح فقط.
</div>
{hint ? <div className="mt-3 text-xs font-medium text-violet-700">{hint}</div> : null}
</label>
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-slate-50/90">
{selectedImage ? (
<>
<img
src={selectedImage.url}
alt={selectedImage.file.name}
className="h-[220px] w-full bg-white object-contain"
/>
<div className="space-y-2 border-t border-slate-200 px-4 py-4 text-sm text-slate-600">
<div className="font-medium text-slate-800">{selectedImage.file.name}</div>
<div className="flex items-center justify-between gap-4">
<span>الأبعاد</span>
<span dir="ltr">
{selectedImage.width} × {selectedImage.height}
</span>
</div>
<div className="flex items-center justify-between gap-4">
<span>الحجم</span>
<span dir="ltr">{formatBytes(selectedImage.file.size)}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span>النوع</span>
<span dir="ltr">{selectedImage.file.type || "image/*"}</span>
</div>
</div>
</>
) : (
<div className="flex h-full min-h-[220px] flex-col items-center justify-center gap-3 px-5 text-center text-sm text-slate-500">
<div className="rounded-2xl bg-white p-4 shadow-sm">
<svg viewBox="0 0 24 24" className="h-8 w-8 fill-none stroke-slate-400" strokeWidth="1.7">
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="m7 15 3-3 3 3 4-5 2 3" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="9" cy="9" r="1.2" fill="currentColor" className="text-slate-400" />
</svg>
</div>
<div>ستظهر هنا معاينة الصورة بعد اختيارها.</div>
</div>
)}
</div>
</div>
</div>
);
}
/**
* مكون عرض النتيجة وتنزيلها (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 (
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
<div>
<h3 className="text-base font-semibold text-slate-900">{title}</h3>
<p className="mt-1 text-sm leading-6 text-slate-600">{description}</p>
</div>
{asset ? (
<div className="mt-4 grid gap-4 lg:grid-cols-[1fr_260px]">
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-slate-50/80">
<img src={asset.url} alt={asset.fileName} className="max-h-[340px] w-full bg-white object-contain" />
</div>
<div className="space-y-4 rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
<div>
<div className="text-sm font-semibold text-slate-900">{asset.fileName}</div>
<div className="mt-1 text-sm text-slate-500">تم إنشاء نسخة PNG جديدة جاهزة للتنزيل والاستخدام.</div>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
<StatPill label="الحجم النهائي" value={formatBytes(asset.size)} />
<StatPill label="الأبعاد" value={`${asset.width} × ${asset.height}`} />
{typeof asset.messageBytes === "number" ? (
<StatPill label="النص المضمّن" value={formatBytes(asset.messageBytes)} />
) : null}
{metadata ? <StatPill label="المساحة المستخدمة" value={formatBytes(metadata.usedBytes)} /> : null}
</div>
<button
type="button"
onClick={onDownload}
className="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-slate-900 px-4 py-3 text-sm font-semibold text-white transition hover:bg-slate-800"
>
<svg viewBox="0 0 24 24" className="h-4 w-4 fill-none stroke-current" strokeWidth="1.8">
<path d="M12 4v11" strokeLinecap="round" />
<path d="m7 10 5 5 5-5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M5 20h14" strokeLinecap="round" />
</svg>
{buttonLabel}
</button>
</div>
</div>
) : (
<div className="mt-4 rounded-3xl border border-dashed border-slate-200 bg-slate-50/80 px-6 py-12 text-center text-sm leading-7 text-slate-500">
لا توجد نتيجة حتى الآن. عند تنفيذ العملية ستظهر هنا معاينة النسخة الجديدة مع زر التنزيل.
</div>
)}
</div>
);
}
export default function App() {
const [mode, setMode] = useState<Mode>("hide");
// حالات الإخفاء (Embedding states)
const [encodeImage, setEncodeImage] = useState<SelectedImage | null>(null);
const [encodeText, setEncodeText] = useState("");
const [encodeStatus, setEncodeStatus] = useState("");
const [encodeError, setEncodeError] = useState("");
const [encodeBusy, setEncodeBusy] = useState(false);
const [encodedAsset, setEncodedAsset] = useState<GeneratedAsset | null>(null);
const [encodedMetadata, setEncodedMetadata] = useState<HiddenTextMetadata | null>(null);
// حالات الاستخراج والتنظيف (Extracting & Cleaning states)
const [decodeImage, setDecodeImage] = useState<SelectedImage | null>(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<ExtractedHiddenText | null>(null);
const [cleanedAsset, setCleanedAsset] = useState<GeneratedAsset | null>(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 (
<div
dir="rtl"
className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(124,58,237,0.16),_transparent_34%),linear-gradient(180deg,_#f8fafc,_#eef2ff_45%,_#ffffff)] text-slate-900"
>
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<header className="overflow-hidden rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-violet-100/80 backdrop-blur sm:p-8">
<div className="grid gap-8 lg:grid-cols-[1.3fr_0.9fr] lg:items-center">
<div>
<div className="inline-flex items-center gap-2.5 rounded-full border border-violet-200 bg-violet-50 px-4 py-2 text-sm font-semibold text-violet-700">
<span className="flex h-3 w-3 items-center justify-center rounded-full bg-violet-500 text-[8px] text-white">
S
</span>
<span>ستيجانو بكسل (SteganoPixel)</span>
</div>
<h1 className="mt-5 text-3xl font-black tracking-tight text-slate-950 sm:text-5xl">
ضع النص داخل أي صورة واستخرجه أو أزله من نفس التطبيق
</h1>
<p className="mt-4 max-w-3xl text-base leading-8 text-slate-600 sm:text-lg">
تطبيق ويب عربي متقدم لإخفاء النصوص السرية داخل الصور الرقمية بأسلوب غير ملحوظ بصرياً (LSB Steganography)،
مع إمكانية قراءة النصوص المضمنة لاحقاً أو محوها بالكامل لتأمين خصوصيتك. تعمل جميع خوارزميات التشفير
مباشرة داخل متصفحك دون إرسال أي ملفات إلى خوادم خارجية.
</p>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-slate-600">
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
حفظ بصيغة PNG Lossless للحفاظ على البتات الدقيقة
</div>
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
يدعم التضمين · الاستخراج · إزالة الدمج (التنظيف)
</div>
<div className="rounded-full border border-slate-200 bg-slate-50 px-4 py-2">
خصوصية تامة 100% بدون اتصال بالإنترنت
</div>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<StatPill label="خوارزمية الدمج" value="3-Channel LSB" />
<StatPill label="بيئة المعالجة" value="Client-Side Browser" />
<StatPill label="دعم التصدير" value="PNG TrueColor (Lossless)" />
<StatPill label="مستوى الأمان" value="Zero Server Uploads" />
</div>
</div>
</header>
<div className="mt-8 grid gap-6 xl:grid-cols-[1.55fr_0.85fr]">
<section className="rounded-[2rem] border border-white/70 bg-white/80 p-4 shadow-2xl shadow-slate-200/60 backdrop-blur sm:p-6">
<div className="flex flex-wrap gap-3 rounded-3xl bg-slate-100/80 p-2">
<button
type="button"
onClick={() => setMode("hide")}
className={`flex-1 rounded-2xl px-4 py-3 text-sm font-semibold transition sm:flex-none sm:min-w-[180px] ${
mode === "hide"
? "bg-slate-900 text-white shadow-lg shadow-slate-300"
: "bg-transparent text-slate-600 hover:bg-white"
}`}
>
تضمين نص سري داخل صورة
</button>
<button
type="button"
onClick={() => setMode("extract")}
className={`flex-1 rounded-2xl px-4 py-3 text-sm font-semibold transition sm:flex-none sm:min-w-[180px] ${
mode === "extract"
? "bg-slate-900 text-white shadow-lg shadow-slate-300"
: "bg-transparent text-slate-600 hover:bg-white"
}`}
>
استخراج النص أو تنظيف الصورة
</button>
</div>
{mode === "hide" ? (
<div className="mt-6 space-y-6">
<UploadCard
title="1) اختر الصورة الأصلية (الناقل)"
description="ارفع الصورة التي تريد دمج النص داخلها. سيقوم التطبيق بقراءة أبعادها وحساب السعة التخزينية المتاحة لتضمين رسالتك."
selectedImage={encodeImage}
onSelectFile={(file) => {
void handleEncodeImageSelect(file);
}}
onClear={() => {
setEncodeImage(null);
setEncodedAsset(null);
setEncodedMetadata(null);
setEncodeStatus("");
setEncodeError("");
}}
hint={
encodeImage?.file.type.includes("jpeg")
? "تنبيه: سيتم تحويل صورتك الـ JPG إلى PNG عند الحفظ لضمان عدم تلف البيانات المخفية بسبب ضغط الـ JPG الضياعي."
: undefined
}
/>
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-slate-900">2) اكتب النص السري المراد إخفاؤه</h3>
<p className="mt-1 text-sm leading-6 text-slate-600">
تُشفّر الرسالة رقمياً وتُوزّع بانتظام على البتات الأقل أهمية للألوان (RGB)، ما يحافظ على النقاء
البصري.
</p>
</div>
<div
className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-2 text-sm text-slate-600"
dir="ltr"
>
{messageBytes} / {capacityBytes || 0} bytes
</div>
</div>
<textarea
value={encodeText}
onChange={(event) => setEncodeText(event.target.value)}
placeholder="اكتب رسالتك السرية هنا..."
className="mt-4 min-h-[180px] w-full rounded-3xl border border-slate-200 bg-slate-50/80 px-4 py-4 text-sm leading-7 text-slate-800 outline-none transition placeholder:text-slate-400 focus:border-violet-400 focus:bg-white focus:ring-4 focus:ring-violet-100"
/>
<div className="mt-4 space-y-3">
<div className="flex items-center justify-between text-xs font-medium text-slate-500">
<span>نسبة استهلاك السعة التخزينية</span>
<span dir="ltr">{usagePercent.toFixed(1)}%</span>
</div>
<div className="h-3 overflow-hidden rounded-full bg-slate-100">
<div
className={`h-full rounded-full transition-all ${
messageBytes > capacityBytes && capacityBytes > 0
? "bg-rose-500"
: "bg-gradient-to-r from-violet-500 to-indigo-600"
}`}
style={{ width: `${Math.min(100, usagePercent)}%` }}
/>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<StatPill label="السعة الكلية المتاحة" value={formatBytes(capacityBytes)} />
<StatPill label="حجم الرسالة الحالي" value={formatBytes(messageBytes)} />
<StatPill label="صيغة التصدير" value="PNG Lossless" />
</div>
</div>
<div className="mt-5 flex flex-wrap gap-3">
<button
type="button"
disabled={encodeBusy}
onClick={() => {
void handleEncode();
}}
className="inline-flex items-center justify-center rounded-2xl bg-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-violet-200 transition hover:bg-violet-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{encodeBusy ? "جارٍ التشفير والدمج..." : "إنشاء الصورة المضمّنة"}
</button>
<button
type="button"
onClick={() => {
setEncodeText("");
setEncodeStatus("");
setEncodeError("");
}}
className="inline-flex items-center justify-center rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50"
>
مسح النص
</button>
</div>
{encodeStatus ? (
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
{encodeStatus}
</div>
) : null}
{encodeError ? (
<div className="mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700">
{encodeError}
</div>
) : null}
</div>
<DownloadCard
title="3) النتيجة النهائية الجاهزة"
description="انقر أدناه لتنزيل نسختك الجديدة من الصورة. تحتوي هذه الصورة الآن على النص السري المضمن وبأعلى جودة بصرية."
asset={encodedAsset}
metadata={encodedMetadata}
buttonLabel="تنزيل الصورة المضمّنة (PNG)"
onDownload={() => {
if (encodedAsset) triggerDownload(encodedAsset.url, encodedAsset.fileName);
}}
/>
</div>
) : (
<div className="mt-6 space-y-6">
<UploadCard
title="1) اختر الصورة المراد فحصها أو تنظيفها"
description="ارفع الصورة المشتبه باحتوائها على نص مخفي. يمكنك استخراج وقراءة محتواها، أو محو البيانات السرية لإنتاج نسخة نظيفة."
selectedImage={decodeImage}
onSelectFile={(file) => {
void handleDecodeImageSelect(file);
}}
onClear={() => {
setDecodeImage(null);
setExtracted(null);
setCleanedAsset(null);
setDecodeStatus("");
setDecodeError("");
setCopied(false);
}}
/>
<div className="rounded-3xl border border-slate-200 bg-white/85 p-4 shadow-lg shadow-slate-200/50">
<div className="flex flex-wrap gap-3">
<button
type="button"
disabled={decodeBusy}
onClick={() => {
void handleExtract();
}}
className="inline-flex items-center justify-center rounded-2xl bg-indigo-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-200 transition hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{decodeBusy ? "جارٍ الفحص والاستخراج..." : "استخراج وقراءة النص السري"}
</button>
<button
type="button"
disabled={cleanBusy}
onClick={() => {
void handleClean();
}}
className="inline-flex items-center justify-center rounded-2xl border border-slate-200 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
>
{cleanBusy ? "جارٍ محو البيانات..." : "إنشاء نسخة نظيفة خالية من النصوص"}
</button>
</div>
{decodeStatus ? (
<div className="mt-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
{decodeStatus}
</div>
) : null}
{decodeError ? (
<div className="mt-4 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-medium text-rose-700">
{decodeError}
</div>
) : null}
<div className="mt-5 rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-slate-900">محتوى النص المستخرج</h3>
<p className="mt-1 text-sm leading-6 text-slate-600">
إذا تم العثور على حزمة بيانات صحيحة في الصورة، سيتم تفكيكها وعرضها هنا.
</p>
</div>
<button
type="button"
disabled={!extracted?.message}
onClick={() => {
void handleCopy();
}}
className="rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
>
{copied ? "تم النسخ بنجاح" : "نسخ النص"}
</button>
</div>
<textarea
readOnly
value={extracted?.message ?? ""}
placeholder="بعد رفع الصورة والنقر على «استخراج وقراءة النص السري»، ستظهر الرسالة المستخرجة هنا إن وجدت."
className="mt-4 min-h-[180px] w-full rounded-3xl border border-slate-200 bg-white px-4 py-4 text-sm leading-7 text-slate-800 outline-none placeholder:text-slate-400"
/>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<StatPill label="حجم النص المستخرج" value={formatBytes(extracted?.messageBytes ?? 0)} />
<StatPill label="حجم الحزمة المشفرة" value={formatBytes(extracted?.usedBytes ?? 0)} />
<StatPill label="أبعاد الصورة" value={extracted ? `${extracted.width} × ${extracted.height}` : "—"} />
</div>
</div>
</div>
<DownloadCard
title="2) النسخة النظيفة المُعقمة"
description="إذا قمت بتنظيف الصورة، يمكنك تنزيل النسخة الجديدة أدناه. تم تصفير جميع بتات البيانات المضمنة فيها لضمان عدم إمكانية استخراجها مجدداً."
asset={cleanedAsset}
metadata={extracted}
buttonLabel="تنزيل الصورة النظيفة (PNG)"
onDownload={() => {
if (cleanedAsset) triggerDownload(cleanedAsset.url, cleanedAsset.fileName);
}}
/>
</div>
)}
</section>
<aside className="space-y-6">
<div className="rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-slate-200/60 backdrop-blur">
<h2 className="text-xl font-bold text-slate-950">آلية عمل التطبيق (LSB Architecture)</h2>
<div className="mt-5 space-y-4">
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
<div className="text-sm font-bold text-slate-900">1. التشفير والتضمين (Embedding)</div>
<p className="mt-2 text-sm leading-7 text-slate-600">
يحول التطبيق حروفك إلى بايتات رقمية، ويضيف إليها توقيعاً سحرياً (`TXTIMG1`) وطول الرسالة، ثم يضعها
بانتظام في البت الأقل أهمية (LSB) لقنوات الألوان (RGB) لكل بكسل، ما يحافظ على دقة الألوان الأصلية.
</p>
</div>
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
<div className="text-sm font-bold text-slate-900">2. القراءة والاستخراج (Extracting)</div>
<p className="mt-2 text-sm leading-7 text-slate-600">
عند الفحص، يقرأ المحرك البكسلات بالترتيب باحثاً عن التوقيع السحري. إذا تطابق التوقيع، يقرأ طول
الرسالة ويستخرج البتات المتبقية ليعيد تكوين النص الأصلي بدقة متناهية.
</p>
</div>
<div className="rounded-3xl border border-slate-200 bg-slate-50/80 p-4">
<div className="text-sm font-bold text-slate-900">3. محو البيانات (Sanitization)</div>
<p className="mt-2 text-sm leading-7 text-slate-600">
أداة التنظيف تقوم بحساب البتات التي تم استخدامها في الصورة وتصفرها تماماً (`& 0xFE`)، ما يعيد
البكسلات لحالة عشوائية طبيعية ويمنع أي محاولة مستقبلية لاستعادة النص.
</p>
</div>
</div>
</div>
<div className="rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-2xl shadow-violet-100/70 backdrop-blur">
<h2 className="text-xl font-bold text-slate-950">ضوابط الحفاظ على الجودة</h2>
<ul className="mt-4 space-y-3 text-sm leading-7 text-slate-600">
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
يتم تصدير الصور حصراً بصيغة <span className="font-semibold text-slate-900">PNG Lossless</span>.
حفظها كـ JPG أو إرسالها عبر الواتساب/تليجرام بوضعية الضغط العادي يؤدي إلى تدمير البتات المخفية.
</li>
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
كلما كانت دقة الصورة وأبعادها أكبر، زادت سعتها التخزينية لاحتواء نصوص سرية أطول ومقالات كاملة.
</li>
<li className="rounded-2xl border border-slate-200 bg-slate-50/80 px-4 py-3">
التلاعب اللوني لا يتجاوز 1/255 من قيمة كل لون، وهو تغيير مستحيل الإدراك بالعين البشرية المجردة.
</li>
</ul>
</div>
<div className="rounded-[2rem] border border-white/70 bg-slate-950 p-6 text-white shadow-2xl shadow-slate-300/60">
<h2 className="text-xl font-bold">نصيحة أمنية واحترافية</h2>
<p className="mt-3 text-sm leading-7 text-slate-300">
لتبادل الرسائل السرية بنجاح، أرسل الصورة النهائية كـ «ملف» (File / Document) في برامج المحادثة أو البريد
الإلكتروني، لضمان عدم قيام تلك المنصات بضغط الصورة وإتلاف البيانات.
</p>
</div>
</aside>
</div>
</div>
</div>
);
}

45
src/index.css Normal file
عرض الملف

@@ -0,0 +1,45 @@
/**
* الأنماط العامة وإعدادات الخطوط (Global Styles & Typography) - SteganoPixel
* =========================================================================
* الجزء الأول من الشرح الهيكلي:
* يوفر هذا الملف التهيئة الأساسية للمشروع بالاعتماد على إطار عمل Tailwind CSS (عبر التوجيه @import "tailwindcss").
* نقوم بضبط المتغيرات الجذرية (:root) لتهيئة بيئة العرض التلقائية، وتحديد عائلة الخطوط الافتراضية (Inter و System-UI)
* لضمان عرض النصوص العربية والإنجليزية بأبهى حلة وبأقصى درجات الوضوح على جميع أنظمة التشغيل (Windows, macOS, iOS, Android).
*
* الجزء الثاني من الشرح الهيكلي:
* يعمل هذا الملف كطبقة تنسيق أساسية تمنع التفاوت البصري بين المتصفحات (Cross-Browser Normalization). نقوم بتصفير
* الهوامش الافتراضية (margin: 0)، وضبط السلوك التفاعلي للتمرير (scroll-behavior)، وتحديد الحد الأدنى لعرض الشاشة (320px)
* لضمان عدم انكسار التخطيط الشبكي (Grid/Flex) على الهواتف الذكية القديمة أو الشاشات الضيقة، مع دعم العرض التلقائي للصور.
*/
@import "tailwindcss";
:root {
color-scheme: light;
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-width: 320px;
background: #f8fafc;
}
* {
box-sizing: border-box;
}
button,
input,
textarea {
font: inherit;
}
img {
display: block;
max-width: 100%;
}

24
src/main.tsx Normal file
عرض الملف

@@ -0,0 +1,24 @@
/**
* نقطة الإطلاق الجذرية (Application Bootstrapper) - SteganoPixel
* =================================================================
* الجزء الأول من الشرح الهيكلي:
* يقوم هذا الملف بدور المشغل الأساسي (Bootstrapper) لتطبيق React. يعتمد على أحدث واجهات البرمجة في React 18+ (`createRoot`)
* لتفعيل آلية التصيير المتزامن (Concurrent Rendering). كما يغلف شجرة المكونات بالكامل داخل مكون `<StrictMode>` الذي
* يساعد المطورين خلال بيئة التطوير على اكتشاف أي مشاكل خفية في دورة حياة المكونات، أو استخدام أدوات قديمة، أو آثار جانبية غير مرغوبة.
*
* الجزء الثاني من الشرح الهيكلي:
* يعمل هذا الملف كجسر يربط بين المستند الفعلي (`index.html`) ومنطق التطبيق المرئي (`App.tsx`). يقوم باستيراد ملف الأنماط
* الأساسي (`index.css`) لضمان حقن قواعد Tailwind CSS الجذرية قبل تصيير أي عنصر. إذا تعذر العثور على عنصر `#root` في
* المستند، سيتوقف المحرك فوراً، ما يمثل آلية حماية ضد تشغيل التطبيق في بيئة HTML غير متوافقة.
*/
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);

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,
};
}

31
tsconfig.json Normal file
عرض الملف

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["node"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}

19
vite.config.ts Normal file
عرض الملف

@@ -0,0 +1,19 @@
import path from "path";
import { fileURLToPath } from "url";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss(), viteSingleFile()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
});