دعم العربية

From Alrebat

Jump to: navigation, search
صورة:Write.gifلا يزال المقال قيد التحرير و مساعدتك بالمعلومة أو بالتنسيق و التدقيق ستكون محل ترحيب كبير.

في هذا الدرس قررت أن أضيف ملحقا عن موضوع "تعريب" الشاشة النصية. في الحقيقة، و قبل أن نبدأ، أحب أن أوضح أن موضوع التعريب، و بصفة عامة موضوع التدويل Internationalization، هو مجال لوحده و يلزمه كتاب أو أكثر للإحاطة بجوانبه. لكن هذا لن يمنعنا بطبيعة الحال من مقاربتنا له بصفة أولية في هذه المرحلة.

الآن إلى المهم، الهدف هو طباعة الحروف العربية على الشاشة النصية. لكن هذه المهمة ليست بسيطة، بمعنى لا يكفي التوفر على رسوم للحروف وطباعتها على الشاشة. فاللغة العربية كما يعلم الكل، لا تكتب ببساطة كمثيلاتها اللاتينية. وهناك بعض المشاكل المتعلقة بالحروف العربية التي ينبغي معالجتها. في هذا الملحق سنشير إلى بعض من هذه المشاكل و كيفية التغلب عليها. بدون إطالة، فيما يلي المراحل اللازمة لإضافة العربية إلى نواتنا التدريبية.


فهرست

كيف نرسم الحروف العربية


أولا لا بد من نظرة عامة على كيفية عمل الفيجا. تعتمد الفيجا على مجموعة من المكونات. كل مكون يقوم بعمل محدد و تمهيدي لعمل المكون الذي بعده:

أولا، الSequencer، يقوم بقراءة البيانات التي قمنا بكتابتها في ذاكرة الفيجا. ثم يحول هذه الأخيرة إلى صيغة قابلة للفهم من قبل المكونات التالية (نحن نكتب فقط أرقام الحروف لكن هذا المكون يقوم بتحويلها إلى نقط Pixels كما سنرى فيما بعد)، تسمى هذه العملية ب Rasterization.

الAttribute Controller يقوم باستعمال بيانات الألوان التي نكتبها في الذاكرة بالإضافة إلى البيانات التي ينتجها الSequencer و من ثم إنتاج عدد من 8 بيت تقدم للمكون الموالي DAC

الDAC يقوم بتحويل ال8 بيتات – التي تعمل كمؤشر على أحد مسجلا الألوان الخاصة به - ة إلى إشارة تماثلية Analog Signal.

ال CRTC يحدد الصيغة المطلوبة لإرسال الإشارة السابقة إلى الشاشة. مثل التردد، Timing...

هناك أيضا مكون آخر يتوفر على مسجلاته الخاصة و هو الGraphics. يمكن برمجته للتحكم في الطريقة التي يقرأ بها المعالج ذاكرة الفيجا.

كل من المكونات المذكورة يوجد على بوابتين فرعيتين و مجموعة مسجلات. للتواصل معه ينبغي أولا كتابة رقم المسجل الذي نريد الولوج إليه في البوابة الفرعية الأولى (بوابة العنونة) ثم كتابة (أو قراءة) البيانات في البوابة الفرعية الثانية (بوابة البيانات).

ما يهمنا من هذا الأمر هو جعل الSequencer يستعمل وصفنا الخاص للحروف لإنتاج رسوم للحروف العربية.

في المرحلة الأولى علينا تحميل رسوم الحروف إلى ذاكرة الفيجا. حيث أن هذا الأخيرة تعتمد لرسم الحروف على الشاشة النصية على مجموعة من الرسوم Bitmap التي تصف شكل الحروف. هذه الرسوم مخزنة في ذاكرة الفيجا و مرتبة على شكل جدول من مداخل بحجم 32 بايت: كل مدخل يصف الحرف المقابل له. مثلا إذا كتبت في ذاكرة الشاشة النصية كما رأينا في الدروس السابقة الرقم 65 الذي يكافئ رقم الحرف A فإن بطاقة الفيجا تقوم باستعمال المدخل 65 (بدءا من 0) لرسم الحرف A على الشاشة.

تقسم ال32 بايت المشكلة لرسم الحرف الواحد إلى سطور. كل بايت يمثل سطرا واحدا: و كل بيت في هذا البايت يشكل نقطة في هذا السطر. تماما كما لو كنت تستعمل جدولا للرسم. كما في الشكل التالي الذي يعطي مثالا لرسم الحرف A

صورة:Letter a.jpg

في هذا الجدول من 8x8 فإن البايتات المشكلة له يمكن أن تكتب على شكل


00111100

01000010

01000010

01111110

01000010


رسوم حروف الفيجا تكون دائما بعرض 8 بيتات (هناك وضع يستعمل 9 بيتات على الشاشة لكن الرسوم تظل بعرض 8 بيتات). لكن ارتفاعها ( وبالتالي عدد البايتات ما دام كل بايت يشكل سطرا واحدا) يختلف ويتم تحديد قيمته في مسجل Maximum scan line و هو أحد مسجلات CRTC ( تذكر الدرس الثالث عن النواة بسي++) تحديدا المسجل رقم 9 في ال5 بيتات الأضعف منه. مما يجعل أقصى قيمة له هي 32. لهذا السبب فكل رسم لحرف يشغل 32 بايت في الذاكرة و يبدأ في عنوان مسطر على 32 بايت. في حال ما كان الارتفاع أقل من 32، و معه رسم الحرف أقل من 32 بايت، فإن الفيجا تستعمل فقط عدد السطور (=البايتات) المطلوبة و تتجاهل السطور المتبقية.


عندما يقلع الحاسب فإن البيوس يقوم بإعداد خارطة لرسوم الحروف – مسبقا – مما يمكن البرامج المنفذة بعده من استعمال الشاشة النصية مباشرة. البيوس يمنح أيضا انقطاعات لتغيير رسم حرف أو مجموعة حروف في ذاكرة الفيجا. لكننا طبعا لا نستطيع استعمالها في الوضع المحمي. مما يحتم علينا التعامل مباشرة مع الفيجا.


بفرض أننا نتوفر على خارطة لرسوم الحروف كيف يمكننا استعمالها؟ الجواب بسيط : عليك بكتابة خارطة البايتات في ذاكرة الفيجا تحديدا في الذاكرة 0xA0000.


الآن السهولة مرتبطة بفهم كيف تعمل ذاكرة الفيجا. و هذا الأمر يتطلب قدرا من التوضيح برغم أننا في ملحق.

ذاكرة الفيجا المستعملة للعرض من حجم 256 كيلوبايت و تبدأ كما قلنا في 0xA0000. في الوضع النصي Text Mode – حيث نكتب حروفا و ندع الفيجا ترسمها بدل الرسم مباشرة - تقسم هذه الذاكرة إلى أربعة قطع من 64 كيلوبايت. تسمى هذه القطع مستويات Planes.


الآن حان الوقت لتعلم أنه عندما نقول أن ذاكرة الفيديو تبدأ في 0xA0000 فهذا لا يعني أننا نتكلم عن الذاكرة الحقيقية RAM. لا يمكننا استعمال الذاكرة الحقيقة في هذا الموقع لأن الفيجا تحجبه. مزيد من التوضيح: تذكر الدرس الثاني، عندما يريد المعالج الولوج إلى موقع في الذاكرة يقوم بإرسال العنوان على ناقلة العناوين ثم يستقبل البيانات من الذاكرة الحية على ناقلة البيانات. لكن يحدث في بعض الأحيان أن بعض الفروع تقوم بمراقبة ناقلة العناوين و قطع طلبات الولوج إلى مناطق معينة من الذاكرة ثم ترسل ما تريد للمعالج كما لو أن البيانات أتت من الذاكرة الحية للجهاز. عندما يريد المعالج كتابة معلومة في الذاكرة فإنها تتعرض لها أيضا و تحول مسارها نحوها كما لو أن المعالج قام بالكتابة في الذاكرة الحية. بعبارة أخرى فإن هذه الفروع تستولي على جزء من فضاء عنونة المعالج. هدف هذه التقنية Memeory Mapped I/O هو أنه يمكنك التواصل مع الفروع عبر قراءة و كتابة المناطق الخاصة بها في الذاكرة. و هذه الطريقة أكثر فعالية من التواصل عبر البوابات الفرعية Port Mapped I/Oحيث أن هذه الأخيرة بطيئة نسبيا، بالإضافة إلى أنها تستلزم تعليمات الأسمبلي in و out. بينما التقنية الأولى تتطلب فقط مؤشرا على الموقع المناسب في الذاكرة.


الآن للعودة إلى موضوعنا فإن الفيجا تستولي على فضاء العنونة من 0xA0000. لكن للمواءمة مع البطاقات الأقدم منها فإن العنوان قد يختلف: مثلا في الوضع النصي بالألوان الذي نستعمله تعنون الفيجا من 0xB8000. لتحديد عنونة الفيجا نستعمل المسجلMiscellaneous Graphics Register و هو المسجل رقم 6 في مسجلات الGraphics. البيتات 2 و 3 من هذا المسجل تحدد عنونة الفيجا:

00: من 0xA0000 إلى 0xBFFFF.

01: من 0xA0000 إلى 0xAFFFF

10: من 0xB0000 إلى 0xB7FFF

11: من 0xB8000 إلى 0xBFFFF


لتحميل الحروف علينا استعمال الوضع 00.


كما قلنا فذاكرة الفيجا تقسم إلى 4 مستويات. كل مستوى من 64 كيلوبايت. تذكر أننا عندما نريد كتابة الحروف في الذاكرة كنا نكتب رقم الحرف متبوعا ببيانات اللون. ما يحدث في الواقع هو أن نظام الGraphics كان يحول مسار العناوين الزوجية إلى المستوى الأول حيث تخزن أكواد الحروف. في حين تحول العناوين الفردية إلى المستوى الثاني حيث تخزن خصائص الحروف من اللون و ما شابه. هذه الطريقة في التأويل تسمى ODD/EVEN و يمكن برمجتها في مسجلات الSequencer (تحديدا البيت رقم 2 من المسجل رقم 4 عندما يوضع على 1 فإنه يعطل هذه الطريقة في التأويل).

لتحميل الحروف سنحتاج لتعطيل هذا الوضع. هناك أيضا مجموعة من المسجلات في الGraphics التي تجعل الفيجا تطبق مجموعة من التحويلات على البيانات من و إلى المعالج و ينبغي أيضا تعطيلها (بوضعها على 0).


رسوم الحروف في الوضع النصي تكون مخزنة في المستوى الثالث (رقم 2 بدءا من 0)، و لتمكيننا من الكتابة في هذا المستوى دون غيره يوجد المسجل رقم 2 في الSequencer. ال4 بيتات الأضعف تتحكم في المستويات التي تكتب فيها البيانات الواردة من المعالج. لا حظ أننا يمكننا الكتابة في أكثر من مستوى دفعة واحدة إذا وضعنا البيتات المكافئة لها على 1. في حالتنا نحتاج لتنشيط المستوى 2 دون غيره مما يعني وضع البيتات على 4 (0100).

للتلخيص


  • ذاكرة الفيجا مقسمة إلى 4 مستويات كل مستوى من 64 كيلوبايت.
  • لمستوى 0 يستعمل لتخزين أرقام الحروف في خارطة الرسوم، المستوى 1 يخزن خصائص الطباعة من اللون و ما شابه. المستوى 2 يخزن خارطة (أو أكثر) رسوم الحروف.
  • في الوضع النصي، تؤول العناوين بطريقة زوجي/فردي Odd/Even حيث ترسل العناوين الزوجية للمستوى 0 والفردية للمستوى 1.
  • عمليا لتحميل خارطة حروف جديدة و استعمالها بدل القديمة:
  • تحديد خارطة العنونة لتبدأ من 0xA0000 بدلا من 0xB8000.
  • تعطيل العنونة ب Odd/Even.
  • اختيار المستوى 2 للكتابة فيه دون غيره.
  • وضع القيم الصحيحة في مسجلات الGraphics لكي لا تؤثر على البيانات التي سنكتبها.
  • كتابة خارطة رسوم الحروف في الذاكرة بدءا من 0xA0000.


التعديل العملى فى الشفرة

في نواتنا التدريبية. سننشئ فئة جديدة FontMap و إسناد هذه المهمة لها عبر الطريقة loadFontMap.

void FontMap::loadDefaultMap(char *fmap) {
     char *mem = (char *) 0xA0000;
}

أولا نقوم بحفظ قيمة المسجلات قبل تغييرها

saveVGARegs();

إعداد الفيجا للكتابة في المستوى رقم 2

outVGA(VGA_SEQ, 0, 1); // Synchronous reset
outVGA(VGA_SEQ, 2, 4); // only write to map 2
outVGA(VGA_SEQ, 4, 7); // Disable O/E, enable Extended memory
outVGA(VGA_SEQ, 0, 3); // clear synchronous reset
outVGA(VGA_GR, 1, 0); // disable set/reset
outVGA(VGA_GR, 3, 0); // logical operation=replace, rotate count=0
outVGA(VGA_GR, 4, 2); // selects map 2 for cpu reads
outVGA(VGA_GR, 5, 0); // write mode=0, read mode=0
outVGA(VGA_GR, 6, 0);

الآن يمكننا نقل خارطة رسوم الحروف. في مثالنا نستعمل حروف بحجم 8x16 أي أن حجم رسم حرف واحد 16 بايت بينما رسم الحرف في الفيجا ينبغي أن يكون مسطرا على 32 بايت.

for (int i = 0; i < 256; ++i) {
     for (int j = 0; j < 16; ++j) {
          mem[i*32+j] = fmap[i*16+j];
     }	
}

إعادة القيم الأصلية للمسجلات.

restoreVGARegs();

للتأكد من أن خارطتنا قد حملت فعلا للذاكرة يمكننا طباعة الحروف من 0 إلى 255 على الشاشة النصية لنرى هل تستعمل الفيجا الرسوم التي قمنا بتحويلها. الصورة التالية تظهر النتيجة

صورة:Letters screen.jpg

المرحلة التالية

بعد أن قمنا بتحميل الحروف يمكننا كتابتها بوضع رقم الحرف الذي نريد في ذاكرة الشاشة النصية. لكن هذا لا يحل كل مشاكلنا. لأن الحروف العربية لها مميزات يعرفها الكل. أولا تكتب من اليمين إلى اليسار في حين أن الفيجا تفترض أن الحروف تكتب من اليسار إلى اليمين لدعم اللغة الإنجليزية. ثانيا الحرف العربي الواحد قد يأخذ أشكالا متعددة حسب وضعه في الكلمة.

عمليا نريد أن نتمكن من القيام بشيء من هذا القبيل في شفرتنا

video->printf(“السلام عليكم”);

و نريد أن تتم معالجة المشاكل بطريقة شفافة لمستعمل printf أو الدوال المتعلقة بالشاشة النصية مثل moveTo(x,y).

هذا يتطلب منا تعديل الشفرة المسؤولة عن التعامل مع الشاشة النصية. في نواتنا الأمر يتعلق بالفئة Video.

فيما يلي سنرى المشاكل التي ترافق مشكلة طباعة الحروف العربة و الحلول التي اخترتها لمعالجتها. أريد فقط أن أشير إلى أنني استعرت بعض التقنيات من شفرة acon في لينوكس.

أولا علينا تعديل شفرة الفئة Video. كما رأينا في الدرس الثالث، فإن هذه الفئة تقوم بطباعة الجمل أو الحروف التي نمدها لها مباشرة في الشاشة النصية. لكننا الآن ملزمون بمعالجة الجمل قبل طباعتها على الشاشة. لأجل ذلك سنقوم بوضع الحروف قبل طباعتها في جدول مسبق.

static CHAR screen[WIDTH*HEIGHT];

هذا الأخير سيمثل الشاشة المنطقية Logical screen أي الحروف كما يتم إمدادنا بها في دوال printf و put. بعد معالجة هذه الحروف سيتم نقلها إلى الشاشة المرئية Visual screen. قبل استعمال العربية، كان هناك تماثل بين الشاشة المنطقية و الشاشة المرئية، لكن الوضع يختلف الآن.

الدالة put لا تكتب الحروف الآن مباشرة في الشاشة المرئية بل في الشاشة المنطقية.

else if(c >= ' ')
{
    screen[_y*width + _x] = c;
   _x++;
} else
    screen[_y*width + _x] = '.';

عندما ننتهي من معالجة الحروف و تحويلها نقوم بنقل النتيجة إلى الشاشة المرئية عبر نداء updateScreen.

void Video::updateScreen() {
    for (int i = yfrom; i <= _y; ++i) {
        renderLine(i, map->process(screen+(i*WIDTH), writeMode));
    }
    yfrom = _y;
}

كما سنرى فيما بعد، فالطريقة process في الفئة FontMap تاخذ سطرا من الشاشة المنطقية و تعيد صيغته المرئية بعد معالجة الحروف.

الدالة renderLine تأخذ الصيغة المرئية للسطر y من FontMap::process و تنقله إلى الشاشة المرئية

inline void renderLine(int y, CHAR *line) {
    for (int j = 0; j < width; ++j) {
        int loc2 = ((y*width)+translate(j))*2;
        mem[loc2] = (line[j]!=0)?line[j]:' ';
        mem[loc2+1] = _attribute;
    }
}

لاحظ أنه لحساب البعد في الشاشة النصية لا نستعمل الصيغة العادية ((i*80)+j)*2. بل نمرر الإحداثي j الذي يمثل الموقع داخل السطر إلى دالة tranaslate. هذه الأخيرة تقوم بحساب البعد المناسب حسب الوضع الذي نوجد فيه عربي أو لاتيني

enum WriteMode { Arabic, Latin };
…

class Video {

…
    WriteMode writeMode;      // تخزن وضع الكتابة المستعمل: عربي أو لاتيني
    FontMap map;
…

    inline int translate(int x) {
        return (writeMode != Arabic)?x:width-x-1;
   };
...

للتلخيص فإننا عندما نطلب طباعة جملة من الفئة Video تقوم هذه الأخيرة بما يلي:

  1. تقوم بطباعة الحروف قبل معالجتها في الشاشة المنطقية
  2. تمرر السطور التي تم تغييرها إلى الطريقة FontMap::process لمعالجة الحروف العربية و ترتيب الحروف.
  3. تطبع النتيجة في الشاشة المرئية مع الأخذ بعين الاعتبار تغيير الإحداثي x في الشاشة المرئية.

لمعالجة سطر من الحروف


هناك مجموعة من المشاكل التي ينبغي مواجهتها:

أول هذا المشاكل هو ترجمة أكواد الحروف التي تمت كتابتها في جمل الشفرة إلى مثيلاتها في خارطة الرسوم التي نستعملها. لفهم المشكل تصور أننا كتبنا التعليمة التالية في الشفرة

video->printf(“Hello”),

السؤال يتعلق بكيف تخزن سلسلة الحروف Hello في ملفنا التنفيذي بعد الترجمة؟ الجواب هو

48 65 6C 6C 6F 00

تحويل الحروف الإنجليزية إلى أرقام يتم طبقا لمعيار ASCII و هذا لا يتغير ما دمنا نستعمل 1 بايت لتخزين الحروف.

الآن ماذا سيحدث إذا كتبنا التعليمة التالية؟

video->printf(“مرحبا”),

في هذه الحالة الجواب قد يختلف تبعا للمعيار الذي سيستخدمه معالج النصوص المستعمل لتشفير الحروف العربية. و هنا يكمن الاختلاف. في الحواسب الشخصية المعايير المعروفة لتشفير الحروف العربية على بايت واحد هي iso-8859-6 و cp1256 هذا الأخير هو المستعمل في ويندوز.

لكن تذكر أن أرقام الحروف التي تعطى لشاشة النصية هي مؤشرات على خارطة الرسوم. مما يلزم علينا تحويل أرقام الحروف المكتوبة في الشفرة إلى ما يكافئها في خارطة الرسوم. لهذا الغرض سنستعمل جدولا يسمى صفحة الأكواد يحتفظ بالأرقام في نظام تشفير معين مثل iso و نظيراتها في خارطة الرسوم. في هذا الجدول سنخزن أيضا بعض المعلومات الإضافية عن الحروف العربية و التي ستفيدنا فيما بعد

أولا بعض define لتحديد أنواع الحروف العربية من حيث تفاعلها مع الحروف المجاورة

      // للحروف التي لا تلتصق مع تاليها مثل الواو، الدال
#define AFT_BREAK 1  
      // للحروف التي لا تلتصق أيضا مع سابقها مثل ء 
#define BEF_BREAK 2  
      // للحروف التي تلتصق مع تاليها و سابقها مثل الباء، السين 
#define JOIN 3                
      // الأعداد
#define NUMBER 5
      // باقي الأنواع أو غير معروفة
#define OTHER 255

هذه البنية تستعمل لتخزين بيانات صفحة الأكواد

struct CodePage {
    DWORD code;
    CHAR glyph;
};


جدول صفحة الأكواد . الحروف مرتبة و مقسمة إلى فئات. كل فئة تبدأ ببنية على شاكلة {255, class}

CodePage iso_8859_6[] = {
    {255,AFT_BREAK},
    {0xc2,0xc2},
    {0xc3,0xc3},
...

    {255,JOIN},
    {0xc6,0xc6},
    {0xc8,0xc8},
    {0xca,0xca},
…
…
    {255,OTHER} // تشير إلى نهاية الجدول
};

لترجمة الأكواد نستعمل الدالة findCharAndType. هذه الدالة تحول الحروف من التشفير المستعمل إلى نظيراتها ي خارطة الرسوم. و تخزن نوع كل حرف في الجدول types.

static CHAR types[WIDTH];    // لتخزين أنواع الحروف
static CHAR newLine[WIDTH];  // هنا توضع الحروف بعد معالجتها
static CHAR natif[WIDTH];    // لتخزين الحروف بعد ترجمتها من صفحة الأكواد
static CHAR *line;           // يضم الحروف المطلوب معالجتها

inline void findCharAndType(CHAR c, int i) {
    for (int j = 0; j < 255; ++j) {
        CodePage *page = defcp+j;

        // إذا كان المدخل يضم 255 فهذه علامة على بداية فئة جديدة
        if(page->code == 255) {
            types[i] = page->glyph;
        } else if(page->code == c) {
            natif[i] = newLine[i] = page->glyph;
            return;
        }
    }
}

الآن حللنا مشكل الترجمة. نمر إلى مشكل تبديل شكل الحرف حسب وضعه في الكلمة : البداية، الوسط... لأجل ذلك نستعمل جدولا يضم الحروف التي يتغير شكلها حسب وضعها في الكلمة و أرقام الرسوم المكافئة لكل وضع

struct GlyphMap {
    CHAR chr, alone, first, middle, last;
};

GlyphMap glyphMap[] = {
     {0xa8,0xa8,0xa8,0xbc,0xa8},
     {0xa9,0xa9,0xa9,0xbd,0xa9},
…

مهمة تغيير شكل الحروف موكولة إلى الدالة

inline void processShapes() {
    for (int i = 0; i < WIDTH; ++i) {
        if(isArabic(natif[i]))
            reshapeCharAt(i);
    }
}

كل ما تقوم به هذه الدالة هو المرور على كل الحروف المطلوب معالجتها و نداء الدالة reshapeCharAt.

inline void reshapeCharAt(int i) {

           // لمعرفة شكل الحرف في موقع ما نحتاج لمعرفة نوع الحرفين المجاورين.
    CHAR xt = (i!=0)?types[i-1]:OTHER;
    CHAR zt = (i!=WIDTH-1)?types[i+1]:OTHER;

        //  نبحث عن المدخل المكافئ في الجدول
    int gm = getGlyphMap(natif[i]);    

       // لا يوجد لا نغير شكل الحرف
    if(gm < 0)    
        return;

            // الآن ننظر هل بإمكاننا ربط الحرف مع الحرف السابق
    if( xt!=AFT_BREAK && xt != OTHER ) {
             //  نعم، يمكننا الربط معه: إذن إما رسم الوسط أو رسم الأخير
             // الآن هل يمكننا الربط مع الحرف التالي
       
         if(zt!=BEF_BREAK && zt != OTHER)
                        // نعم، يمكننا الربط : نختار رسم الوسط
            newLine[i] = glyphMap[gm].middle;  

        else
                        // لا يمكننا الربط مع التالي: نختار رسم الأخير
            newLine[i] = glyphMap[gm].last;       

                  // إذا وصلنا هنا معنى لا يمكننا الربط مع الحرف السابق
                  // إما رسم الأول أو رسم المنعزل
                  // ننظر هل يمكننا الربط مع الحرف التالي
    } else if(zt!=BEF_BREAK && zt != OTHER) {
                  // يمكننا الربط معه، نختار رسم الأول

            newLine[i] = glyphMap[gm].first;     
    } else

                // لا يمكننا الربط معه، نختار رسم المنعزل
            newLine[i] = glyphMap[gm].alone;   
}

حللنا مشكل الربط بين الحروف. المشكل التالي: الحروف التي تجمع في حرف واحد مثل "لا"

الدالة transformPairs ستتيح لنا تحويل حرفين مثل لام و ألف إلى "لا". في مثالنا سنستعمل رسمين منفصلين إذا تم ربطهما يعطيان شكلا أشبه ب "لا". سنستغل أيضا هذه الطريقة لرسم الحروف كبيرة الحجم مثل الشين في آخر السطر متبوعة بفراغ. سنستغل جزء من الفراغ التالي لتكميل رسم الحرف.


struct Pairs {
    CHAR old1, old2, new1, new2;
};

Pairs pairs[] = {
    {0xfa,0x96,0xfa,0xa2},
    {0xfa,0x9d,0xfa,0xa3},
…

inline void transformPairs() {
    CHAR c1, c2;
    for (int i = 0; i < WIDTH-1; i++) {
        i+changePair(i, newLine[i], newLine[i+1]);
    }
}

هذه الدالة تمر على جميع الحروف و تنادي الدالة changePair على كل حرف مع الذي يليه. إذا نجحت هذه الأخيرة فإنها تعيد 1 مما يعني أنه لا حاجة لمعالجة الحرف الموالي

inline int changePair(int ind, CHAR c1, CHAR c2) {
   if(!c2) c2 = ' ';
   for (int j = 0; pairs[j].old1!=0; ++j) {
      if(pairs[j].old1 == c1 && pairs[j].old2 == c2) {
         newLine[ind] = pairs[j].new1;
         newLine[ind+1] = pairs[j].new2;
         return 1;
      }
   }
   return 0;
}

أيضا علينا حل مشكل الأقواس حيث أنه علينا تبديل اتجاهها في اللغة العربية.

// هذه البنية تتيح لنا معرفة القوس المقابل لكل قوس آخر
struct Braces {
    CHAR in, out;
};

Braces braces[] = {
    {  '{'  ,  '}'  } ,
    {  '}'  ,  '{'  } ,
    {  '['  ,  ']'  } ,
    {  ']'  ,  '['  } ,
    {  '('  ,  ')'  } ,
    {  ')'  ,  '('  } ,
    {   '<' ,  '>'  } ,
    {   '>' ,  '<'  } ,
    {0,0}
};

inline CHAR getBrace(CHAR chr) {
    for (int i = 0;braces[i].in!=0 ; ++i) {
        if(braces[i].in == chr) 
            return braces[i].out;
    }
    return chr;
}

inline void transformBraces() {
    for (int i = 0; i < WIDTH; ++i) {
        newLine[i] = getBrace(newLine[i]);
    }
}

الآن يبقى لنا أصعب مشكل و هو تبديل ترتيب الحروف في السطر تبعا لوضع الكتابة. سأعطي مثالا بسيطا لتوضيح المشكل. تصور أن لدينا الكلمة التالية "مرحبا". هذه الكلمة تكون مخزنة بالشكل التالي في الذاكرة

ا ب ح ر م

إذا قمنا بطباعة هذه الجملة في الوضع اللاتيني من اليسار إلى اليمين بدو تبديل حروفها ستطبع على الشكل

ابحرم

عكسا لنتصور أن لدينا كلمة لاتينية “Hello” و قمنا بطباعتها في الوضع العربي من اليمين إلى اليسار بدون تبديل. هذا ما سيظهر على الشاشة

olleH

المطلوب إذن هو عكس ترتيب الحروف العربية عندما نكون في الوضع اللاتيني. و عكس ترتيب الحروف اللاتينية عندما نكون في الوضع العربي. ما يزيد الأمر صعوبة ليس فقط أن السطر الواحد يمكن أن يضم خليطا من الكلمات اللاتينية و العربية. بل أن هناك أيضا مجموعة من الرموز التي لا تتبع لغة معينة مثل + و - ... هناك أيضا مشكل الفراغات بين الكلمات التي تنتمي لنفس اللغة. تصور أن لدينا جملة مثل

السلام عليكم Hello world 

إذا لم نأخذ بعين الاعتبار الفراغات بين الكلمات من نفس اللغة فإن هذه الجملة ستطبع كما يلي

السلام عليكم  world Hello

هناك أيضا مشكل الأعداد التي تكتب دائما من اليسار إلى اليمين. و هذه ليست سوى لمحة من المشاكل التي تواجه المبرمج عندما يريد دعم النص الثنائي الاتجاه Bidirectional Text.

من اجل ترتيب أفكارنا علينا إذن و ضع مجموعة من القواعد الواضحة.

قواعد عامة

  • الحروف العربية تعكس في الوضع اللاتيني.
  • الحروف اللاتينية تكس في الوضع العربي.
  • الفراغات و الرموز التي لا تنتمي للغة معينة تتبع – مسبقا – اتجاه الكتابة في الوضع الجاري. لكن إذا كانت محصورة بين حروف لغة معينة فإنها تتبع حروف هذه اللغة.
  • الأرقام تعالج مثل الحروف اللاتينية في الوضع العربي. لكن في الوضع اللاتيني إذا كانت محصورة بين حروف عربية تظل وسط الحروف من دون أن تقلب ترتيب الكلمات العربية. مثلا إذا كان لدينا الجملة التالية في الوضع اللاتيني "حجم الذاكرة 32568 كيلوبايت" و لم نراعي هذه القاعدة فإن النتيجة ستكون كيلوبايت 32568 حجم الذاكرة

الآن لننظر كيف يمكننا تنجيز هذه القواعد. أولا نحتاج لبعض الدوال المساعدة تخبرنا ما إذا كانت الحروف عربية أو لاتينية أو أرقام

inline bool isArabic(CHAR c) {
    return ( (c >= 0xc1 && c <= 0xda) ||  (c>=0xe1 && c<=0xea) );
}

inline bool isLatin(CHAR c) {
    return ( (c >= 'A' && c <= 'Z') || (c>='a' && c<='z'));
}

inline bool isNumber(CHAR c) {
    return (c >= '0' && c <= '9');
}

الدالة reverse تقلب ترتيب len حرف ابتداء من buf

inline void reverse(CHAR *buf, int len) {
    CHAR *head, *tail;
    head = buf;
    tail = buf+len;
    while (head < tail) {
        char tmp = *head;
        *head = *tail;
        *tail = tmp;
        head++;
        tail--;
    }
}

الدالة reverseAr تقلب ترتيب الحروف العربية و ما داخلها من فراغات و رموز. الأرقام تظل بترتيبها الأصلي

inline void reverseAr(CHAR *buf, int len1) {
            // أولا نقلب ترتيب كل الحروف بما فيهل الأرقام
      reverse(buf, len1);

           // ثم نبحث عن الأرقام لإعادتها إلى ترتيبها الأصلي
    for (int i = 0; i < len1; ++i) {
        if(isNumber(buf[i])) {
            int x = i;
                // نمر على جميع الأرقام و ما يمكن أن تحتويه
                //  داخلها من فراغات و فواصل و نقط
            for (;  x < len1 &&
                      (isNumber(buf[x]) ||  buf[x] == ‘ ‘ ||
                       buf[x] == ‘.‘ || buf[x] == ‘,’ ) ;  ++x);

                     // بعد الخروج من الحلقة
                    //  x-1 تشير إلى آخر رقم أو نهاية الجملة 
            reverse(&buf[i], x-i-1);
            i = x-1;
        }
    }
}

الشفرة الرئيسية تكمن في الطريقة FontMap::process التي تنادى من قبلVideo->updateScreen لمعالجة سطر من سطور الشاشة المنطقية قبل نقله على الشاشة المرئية.

CHAR* FontMap::process(CHAR *buf, WriteMode mode) {
    line = buf;    
    CHAR c;

              // أولا ترجمة صفحة الأكواد + معرفة أنواع الحروف 
    for (int i = 0; i < WIDTH; ++i) {
        c=line[i];
        if(c < 127) {
            natif[i] = newLine[i] = c;
            types[i] = OTHER;
    } else
        findCharAndType(c, i);
    }

              //   ثانيا، اختيار الشكل الصحيح للحروف حسب أوضاعها داخل الكلمة
    processShapes();

              //  ثالثا، معالجة الحروف المترابطة مثل لام ألف
    transformPairs();
    if(mode == Arabic) {
               //رابعا، في الوضع العربي نحتاج لتبديل اتجاه الاقواس
        transformBraces();

                //  خامسا، قلب ترتيب الحروف اللاتينية و الأرقام
        processBidiAr();
    } else
                //   رابعا، في الوضع اللاتيني نحتاج فقط لقلب الحروف العربية
        processBidiLat();
     
       // عيد السطر لطباعته في الشاشة المرئية
    return newLine;
}

الدالة processBidiAr تعالج نصا ثنائي الاتجاه في الوضع العربي

inline void processBidiAr() {
    int endLatin = 0; // لتخزين قيمة آخر حرف لاتيني

    for (int i = 0; i < WIDTH; ++i) {

               //  عند مصادفة بداية كلمة لاتينية أو عدد نقلب ترتيبها
        if (isLatin(natif[i]) || isNumber(natif[i])) {
            int j = endLatin = i;

                // نستمر في الحلقة حتى أول حرف عربي
            for (; j < WIDTH && !isArabic(natif[j]) && natif[j]!=0; ++j) {
                if(isLatin(natif[j]) || isNumber(natif[j]))
                    endLatin = j;
            }
               //  تشير إلى موقع آخر كلمة لاتينية endLatin 
               //  تشير إلى موقع أول كلمة عربية j-1 
            reverse(&newLine[i], endLatin-i);
            i = j-1;
        }
    }
}

معالجة النص في الوضع اللاتيني

inline void processBidiLat() {
    int endArabic = 0; // لتخزين موقع آخر حرف عربي
    
    for (int i = 0; i < WIDTH; ++i) {
        if(isArabic(natif[i])) {
            int j = endArabic = i;
            for (; j < WIDTH && !isLatin(natif[j]) && natif[j]!=0 ; ++j) {
                if(isArabic(natif[j]))
                    endArabic = j;
            }
                // تشير إلى موقع آخر حرف عربي endArabic 
                // تشير إلى موقع أول حرف لاتيني j-1 

                 // لقلب ترتيب الحروف مع الحفاظ على ترتيب
                 // الأرقام داخل الجملة العربية
            reverseAr(&newLine[i], endArabic-i);
            i = j-1;
        }
    }
}

الآن أتممنا تنجيزنا للتعريب في الشاشة النصية بشكل مقبول. قبل المرور إلى التجربة كلمة أخيرة بخصوص صفحات الأكواد المستعملة: في الشفرة المرافقة للدرس أدمجت صفحتين للأكواد هما iso-8859-6 و cp1256. بما أنني أعمل في Eclipse في بيئة ويندوز (أتمنى ألا يغضب مني مستعملو لينوكس). و محرر Eclispse يتيح لي فقط الكتابة ب cp1256. فجميع الجمل العربية التي كتبت مشفرة بهذا المعيار. لذلك فبرنامج النواة يستعمل – مسبقا – cp1256. إذا أردت تجريب الشفرة و إضافة الجمل إليها باستعمال iso-8859-6 فقد قمت بإدخال التعليمة التالية في مصنع الفئة FontMap

FontMap::FontMap() {
    #ifdef ISO
        defcp = iso_8859_6
    #else
        defcp = cp1256;
    #endif
}

هذا يعني أنه عند الترجمة عليك أن تعرف ISO. بإضافة –DISO على تعليمة الترجمة ب gcc. لا تنسى أيضا تبديل الجمل الحالية باستعمال محرر يدعم iso-8859-6.

صورة:Kernel msgs in arabic on display.jpg

منظر العربية دائما مميز و إن كان على الشاشة النصية. ما رأي عشاق لغة القرآن؟

Personal tools