UP | HOME
جناوي

السيد مذهل
mr. fantastic

طالب هندسة برمجيات - مهتم بالرسوميات والهندسة الصوتية

Software engineering student - interested in graphics and audio engineering

شفرة رسم الخطوط الرقمية البدائية
نشر في Feb 18, 2021 بقلم السيد مذهل.

تطورت تقنيات الكتابة على مراحل عديدة فسابقاُ لم يكن هناك سوى عدد محدود من الحروف والرموز التي يمكن أستخدامها فكان هناك بعض الأجهزة التي لا تحوي سوى الأحرف والرموز الأنجليزية بشكلها الكبير (upper case letters) مثل حاسوب كومودور أربعةٌ وستون (Commodore 64 [C64])

commodore64.jpg

شكل 1: لوحة مفاتيح كومودور 64

وفي ما مضى لم يكن هناك إي معيار يحدد مجموعة الرموز حتى ظهور آسكي (ASCII) وفيما بعد اليونيكود (Unicode) بعد فترة من الزمن, وهنا نرى آسكي كمثال جيد وبسيط لأن كل حروفه ورموزه يمكن تخزينها ببايت واحد فقط على عكس اليونيكود الذي يشمل الآسكي وأكثر حيث على سبيل المثال أغلب أن لم تكن كل الحروف والرموز العربية تخزن بباتين أثنين ولهذا اليونيكود معقد لأن بعض الرموز تأخذ حيز بايت أو بايتين أو ثلاثة أو أكثر حسب اللغة ومعيار يونيكود ثمانية (UTF-8) أو معايير اليونيكود الأخرى لهذا لن يكون مثالاً جيداً لنبدأ به.

وفي ضمن سياق تطور تقنيات الكتابة كانت الخطوط وملفات الخطوط تعتمد على صيغ نقطية بكسلية (bitmap, pixel based) ومن ثمة تحولت إلى صيغ متجهة تشبه البوست سكربت ومعيار الأس في جي (SVG) وعندما كتبت العنوان وهو يحوي الخطوط البدائية فأنا أعني الخطوط البكسلية كما هي مطبقة بالألعاب الكلاسكية مثل فريدوم (FreeDoom)

freedoom.png

شكل 2: لقطة شاشة من فريدوم

كفى تاريخ

كفى تاريخ ولندخل بصلب الموضوع سوف نقوم بأستخدام صيغة netpbm للصور لنطبق الرسم برنامجنا وسوف أستخدم جيمب Gimp حتى نصنع صورة تحوي على مجموعة جزئية من حروف ورموزه آسكي مصفوفة بترتيب معين.

مجموعتنا الجزئية ستحوي فقط الحروف الأنجليزية الكبيرة والأرقام وبعض الرموز وهذا لتبسيط المثال ليس إلا, ولنقوم بهذا سأجهز صورة بمقاس 95 عرضاً و 15 بكسل طولاً بمعنى أن كل حرف أو رمز له مساحة 5x5 بكسل حيث سيكون لدينا 57 بين حرف ورمز

fontedit.png

شكل 3: صورة مقربة لملف الخط

سوف نصدر هذه الصورة بصيغة netpbm الذي سبق وشرحتها بهذه المدونة المتواضعة وكان سبب الأختيار هو أنها غير مضغوطة ومن السهل قراءتها والكتابة بها وهذا رابط تنزيل الصورة (كثير من المتصفحات لا تدعم عرض صورة ppm لذلك قد تظهر الصورة لك على شكل رابط):

../images/posts/bitmap-font/5x5.ppm

ولاحقاً بالشفرة المصدرية سوف نلصق كل لون ليس بأبيض كامل على الصورة المراد الكتابة عليها (تماماً كرسومات الجرافيتي بالشوارع)

// -*- compile-command: "gcc -o main main.c && ./main 5x5.ppm && eog blackboard.ppm"; -*-
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// backslash, semi colon are missing from the font

#define SIZE 5
#define SPACING 1+5

unsigned char *data;

static unsigned char img[400][400][3];

#define CHAR_A data + (95*0) + 0
#define CHAR_B data + (95*0) + 15
#define CHAR_C data + (95*0) + 30
#define CHAR_D data + (95*0) + 45
#define CHAR_E data + (95*0) + 60
#define CHAR_F data + (95*0) + 75
#define CHAR_G data + (95*0) + 90
#define CHAR_H data + (95*0) + 105
#define CHAR_I data + (95*0) + 120
#define CHAR_J data + (95*0) + 135
#define CHAR_K data + (95*0) + 150
#define CHAR_L data + (95*0) + 165
#define CHAR_M data + (95*0) + 180
#define CHAR_N data + (95*0) + 195
#define CHAR_O data + (95*0) + 210
#define CHAR_P data + (95*0) + 225
#define CHAR_Q data + (95*0) + 240
#define CHAR_R data + (95*0) + 255
#define CHAR_S data + (95*0) + 270

#define CHAR_T data + (95*15) + 0
#define CHAR_U data + (95*15) + 15
#define CHAR_V data + (95*15) + 30
#define CHAR_W data + (95*15) + 45
#define CHAR_X data + (95*15) + 60
#define CHAR_Y data + (95*15) + 75
#define CHAR_Z data + (95*15) + 90
#define CHAR_0 data + (95*15) + 105
#define CHAR_1 data + (95*15) + 120
#define CHAR_2 data + (95*15) + 135
#define CHAR_3 data + (95*15) + 150
#define CHAR_4 data + (95*15) + 165
#define CHAR_5 data + (95*15) + 180
#define CHAR_6 data + (95*15) + 195
#define CHAR_7 data + (95*15) + 210
#define CHAR_8 data + (95*15) + 225
#define CHAR_9 data + (95*15) + 240
#define CHAR_CPAR data + (95*15) + 255 /* ) */
#define CHAR_EXC data + (95*15) + 270  /* ! */


#define CHAR_DOLLAR data + (95*30) + 0 /* $ */
#define CHAR_PERCENT data + (95*30) + 15 /* % */
#define CHAR_HAT data + (95*30) + 30     /* ^ */
#define CHAR_AND data + (95*30) + 45     /* & */
#define CHAR_PLUS data + (95*30) + 60    /* + */
#define CHAR_OPAR data + (95*30) + 75    /* ( */
#define CHAR_COMMA data + (95*30) + 90   /* , */
#define CHAR_DOT data + (95*30) + 105    /* . */
#define CHAR_SLASH data + (95*30) + 120  /* / */
#define CHAR_CBRKT data + (95*30) + 135  /* ] */
#define CHAR_APO data + (95*30) + 150    /* ' */
#define CHAR_OBRKT data + (95*30) + 165  /* [ */
#define CHAR_PIPE data + (95*30) + 180   /* | */
#define CHAR_LT data + (95*30) + 195     /* < */
#define CHAR_BT data + (95*30) + 210     /* > */
#define CHAR_QM data + (95*30) + 225     /* ? */
#define CHAR_DQ data + (95*30) + 240     /* " */
#define CHAR_CBC data + (95*30) + 255    /* } */
#define CHAR_CBO data + (95*30) + 270    /* { */

void print_char (int x, int y, unsigned char *pos)
{
  unsigned char *ptr = pos;

  for (int k = 0; k < 5; ++k)
    {
      for (int j = 0; j < 5; ++j)
        {
          // if (ptr[0] != 255 || ptr[1] != 255 || ptr[2] != 255) // same as below
          if (*ptr != 255 || *(ptr+1) != 255 || *(ptr+2) != 255)
            {
              img[y+k][x+j][0] = *ptr++;
              img[y+k][x+j][1] = *ptr++;
              img[y+k][x+j][2] = *ptr++;
            }
          else
            {
              ptr+=3;
            }
        }
      ptr = pos + (3*(k+1))*95;
    }
}

void add_text (int x, int y, char *text)
{
  for (int i = 0; i < strlen (text); ++i)
    {
      // better than if else (dictionary lookup not branching as terry davis says)
      switch (text[i])
        {
        case 'A':
        case 'a':
          {
            print_char (x, y, CHAR_A);
            break;
          }
        case 'B':
        case 'b':
          {
            print_char (x, y, CHAR_B);
            break;
          }
        case 'C':
        case 'c':
          {
            print_char (x, y, CHAR_C);
            break;
          }
        case 'D':
        case 'd':
          {
            print_char (x, y, CHAR_D);
            break;
          }
        case 'E':
        case 'e':
          {
            print_char (x, y, CHAR_E);
            break;
          }
        case 'F':
        case 'f':
          {
            print_char (x, y, CHAR_F);
            break;
          }
        case 'G':
        case 'g':
          {
            print_char (x, y, CHAR_G);
            break;
          }
        case 'H':
        case 'h':
          {
            print_char (x, y, CHAR_H);
            break;
          }
        case 'I':
        case 'i':
          {
            print_char (x, y, CHAR_I);
            break;
          }
        case 'J':
        case 'j':
          {
            print_char (x, y, CHAR_J);
            break;
          }
        case 'K':
        case 'k':
          {
            print_char (x, y, CHAR_K);
            break;
          }
        case 'L':
        case 'l':
          {
            print_char (x, y, CHAR_L);
            break;
          }
        case 'M':
        case 'm':
          {
            print_char (x, y, CHAR_M);
            break;
          }
        case 'N':
        case 'n':
          {
            print_char (x, y, CHAR_N);
            break;
          }
        case 'O':
        case 'o':
          {
            print_char (x, y, CHAR_O);
            break;
          }
        case 'P':
        case 'p':
          {
            print_char (x, y, CHAR_P);
            break;
          }
        case 'Q':
        case 'q':
          {
            print_char (x, y, CHAR_Q);
            break;
          }
        case 'R':
        case 'r':
          {
            print_char (x, y, CHAR_R);
            break;
          }
        case 'S':
        case 's':
          {
            print_char (x, y, CHAR_S);
            break;
          }
        case 'T':
        case 't':
          {
            print_char (x, y, CHAR_T);
            break;
          }
        case 'U':
        case 'u':
          {
            print_char (x, y, CHAR_U);
            break;
          }
        case 'V':
        case 'v':
          {
            print_char (x, y, CHAR_V);
            break;
          }
        case 'W':
        case 'w':
          {
            print_char (x, y, CHAR_W);
            break;
          }
        case 'X':
        case 'x':
          {
            print_char (x, y, CHAR_X);
            break;
          }
        case 'Y':
        case 'y':
          {
            print_char (x, y, CHAR_Y);
            break;
          }
        case 'Z':
        case 'z':
          {
            print_char (x, y, CHAR_Z);
            break;
          }
        case '0':
          {
            print_char (x, y, CHAR_0);
            break;
          }
        case '1':
          {
            print_char (x, y, CHAR_1);
            break;
          }
        case '2':
          {
            print_char (x, y, CHAR_2);
            break;
          }
        case '3':
          {
            print_char (x, y, CHAR_3);
            break;
          }
        case '4':
          {
            print_char (x, y, CHAR_4);
            break;
          }
        case '5':
          {
            print_char (x, y, CHAR_5);
            break;
          }
        case '6':
          {
            print_char (x, y, CHAR_6);
            break;
          }
        case '7':
          {
            print_char (x, y, CHAR_7);
            break;
          }
        case '8':
          {
            print_char (x, y, CHAR_8);
            break;
          }
        case '9':
          {
            print_char (x, y, CHAR_9);
            break;
          }
        case ')':
          {
            print_char (x, y, CHAR_CPAR);
            break;
          }
        case '!':
          {
            print_char (x, y, CHAR_EXC);
            break;
          }
        case '$':
          {
            print_char (x, y, CHAR_DOLLAR);
            break;
          }
        case '%':
          {
            print_char (x, y, CHAR_PERCENT);
            break;
          }
        case '^':
          {
            print_char (x, y, CHAR_HAT);
            break;
          }
        case '&':
          {
            print_char (x, y, CHAR_AND);
            break;
          }
        case '+':
          {
            print_char (x, y, CHAR_PLUS);
            break;
          }
        case '(':
          {
            print_char (x, y, CHAR_OPAR);
            break;
          }
        case ',':
          {
            print_char (x, y, CHAR_COMMA);
            break;
          }
        case '.':
          {
            print_char (x, y, CHAR_DOT);
            break;
          }
        case '/':
          {
            print_char (x, y, CHAR_SLASH);
            break;
          }
        case ']':
          {
            print_char (x, y, CHAR_CBRKT);
            break;
          }
        case '\'':
          {
            print_char (x, y, CHAR_APO);
            break;
          }
        case '[':
          {
            print_char (x, y, CHAR_OBRKT);
            break;
          }
        case '|':
          {
            print_char (x, y, CHAR_PIPE);
            break;
          }
        case '<':
          {
            print_char (x, y, CHAR_LT);
            break;
          }
        case '>':
          {
            print_char (x, y, CHAR_BT);
            break;
          }
        case '?':
          {
            print_char (x, y, CHAR_QM);
            break;
          }
        case '"':
          {
            print_char (x, y, CHAR_DQ);
            break;
          }
        case '}':
          {
            print_char (x, y, CHAR_CBC);
            break;
          }
        case '{':
          {
            print_char (x, y, CHAR_CBO);
            break;
          }

        default:
          break;
        }
      x+=SPACING;
    }
}

int main(int argc, char **argv)
{
  if (argc < 2)
    {
      printf("Usage: %s font.ppm\n", argv[0]);
      exit (0);
    }

  unsigned int w, h, d;
  FILE *fp = fopen(argv[1], "rb");
  char *line = NULL; // initializing it to NULL is very important for memory safty
  size_t len;
  getline (&line, &len, fp);
  if (strcmp ("P6\n", line) != 0)
    { printf("This is not valid P6 ppm file\n"); fclose (fp); exit (1); }
  getline (&line, &len, fp);
  while (strstr(line, "#") != NULL) getline (&line, &len, fp);
  sscanf (line, "%u %u", &w, &h);
  getline (&line, &len, fp);
  sscanf (line, "%u", &d);
  data = malloc (w*h*3);
  fread (data, w*h*3, 1, fp);
  fclose (fp);

  FILE *fpb = fopen("blackboard.ppm", "w+b");
  fprintf(fpb, "P6\n400 400\n255\n");

  for (int i = 0; i < 400; ++i)
    for (int j = 0; j < 400; ++j)
      {img[i][j][0]=i; img[i][j][1]=255; img[i][j][2]=i;}


  add_text (10,10,"Ali sadeq");

  add_text (10,100,"Happy, Hacking!.");

  add_text (10,380,"Donec Suspendisse potenti.  Aliquam erat volutpat.");

  add_text (150, 10, "Join us now and share the software;");
  add_text (150, 20,"You'll be free, hackers, you'll be free.");
  add_text (150, 30,"Join us now and share the software;");
  add_text (150, 40,"You'll be free, hackers, you'll be free.");

  add_text (150, 60,"Hoarders can get piles of money,");
  add_text (150, 70,"That is true, hackers, that is true.");
  add_text (150, 80,"But they cannot help their neighbors;");
  add_text (150, 90,"That's not good, hackers, that's not good.");

  add_text (150, 110,"When we have enough free software");
  add_text (150, 120,"At our call, hackers, at our call,");
  add_text (150, 130,"We'll kick out those dirty licenses");
  add_text (150, 140,"Ever more, hackers, ever more.");

  add_text (150, 160,"Join us now and share the software;");
  add_text (150, 170,"You'll be free, hackers, you'll be free.");
  add_text (150, 180,"Join us now and share the software;");
  add_text (150, 190,"You'll be free, hackers, you'll be free.");

  fwrite (img, 400*400*3, 1, fpb);
  fclose (fpb);

  free (data);
  return 0;
}

سيقوم البرنامج بالبداية بفتح ملف الخط الموجود بنفس المجلد أو مجلد ثاني مع ضرورة تحديد المسار بشكل صحيح ومن ثم سيقوم بأنشاء صورة تدرج لون أخضر عبثية من أجل صنع خلفية ولتوضيح أن الشفرة لا تنسخ المساحات البيضاء من ملف الخط

blackboard.png

شكل 4: نتيجة البرنامج

ملاحظة: الصورة أعلاه تحتوي على بعض النصوص اللاتينية العشوائية وقصيدة البرمجيات الحرة لرتشارد ستالمن وأشياء أخرى

شرح الشفرة

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

humanview.png

شكل 5: كيف يرى الأنسان قطعة الذاكرة

machineview.png

شكل 6: كيف يراها الحاسوب

ولكننا نملك لكل بكسل ثلاث أرقام لتخزين القنوات اللونية (أحمر أخضر أزرق) ولهذا ستكون الحسابات كما يلي

...

#define CHAR_A data + (95*0) + 0
#define CHAR_B data + (95*0) + 15
#define CHAR_C data + (95*0) + 30

...

#define CHAR_T data + (95*15) + 0
#define CHAR_U data + (95*15) + 15
#define CHAR_V data + (95*15) + 30

...

هذا لأن الحرف يأخذ 25 بكسل و 75 بايت لأنه يأخذ حيز 15 بايت طولاً وعرضاً ونحن لدينا تسعة عشرة حرفاً بالصف الواحد ولدينا ثلاث صفوف من الرموز والأحرف.

pos.png

شكل 7: كيف نأخذ البكسلات

ومن هنا يمكننا أخذ البكسلات من زاوية أعلى اليسار

for (int k = 0; k < 5; ++k)
  {
    for (int j = 0; j < 5; ++j)
      {
        // if (ptr[0] != 255 || ptr[1] != 255 || ptr[2] != 255) // same as below
        if (*ptr != 255 || *(ptr+1) != 255 || *(ptr+2) != 255)
          {
            img[y+k][x+j][0] = *ptr++;
            img[y+k][x+j][1] = *ptr++;
            img[y+k][x+j][2] = *ptr++;
          }
        else
          {
            ptr+=3;
          }
      }
    ptr = pos + (3*(k+1))*95;
  }

ويجدر بنا الإشارة إلى أن أستخدام switch لم يأتي عبثاً وهذا لأنها في حالة وجود الكثير من الخيارات وعند الأستطاعة يفضل أستخدامها بدل الـ if و else لأنها switch على عندما يحول المجمع فأن على الأغلب لن تتحول إلى branching

من الواضح أن البرنامج توضيحي فقط لأن يتضمن الكثير من القيم الثابتة (Hard-coded).

عند تخطي حدود الصورة ستحدث أخطاء لم أعتني بمعالجتها وهذا لبساطة المثال.

صورة فريدوم مأخوذة من مشروع فريدوم تحت رخصة بي أس دي (BSD License) 2020.

صورة لوحة مفاتيح كومودور 64 مأخوذة من ويكيميديا تحت رخصة جنو للوثائق الحرة (GFDL 1.2 License).

comments . التعليقات