التحدي الهندسي في توليد مستندات وفواتير PDF باللغة العربية

في أنظمة تخطيط موارد المؤسسات (ERP)، وبرمجيات نقاط البيع (POS)، ومنصات التجارة الرقمية في سوريا والمنطقة، يُعد التوليد التلقائي للفواتير والإيصالات الرقمية جزءاً مهماً من العمل. تتطلب معاملات كثيرة وثائق قابلة للطباعة، وتختلف المتطلبات القانونية بحسب نوع المعاملة والولاية القضائية.

ومع ذلك، يظل توليد ملفات PDF باللغة العربية أحد أكثر التحديات التقنية تعقيداً لمهندسي البرمجيات. فغالباً ما تصطدم الفرق البرمجية بعيوب كلاسيكية عند تحويل المستندات إلى صيغ رقمية:

  1. انفصال الحروف (Disconnected Glyphs): اللغة العربية لغة متصلة تتغير فيها أشكال الحروف وفقاً لموقعها في الكلمة (بداية، وسط، نهاية، أو مفردة). العديد من مكتبات PDF الافتراضية تعجز عن ربط الحروف، فتظهر الكلمات مقطعة (ف ا ت و ر ة بدلاً من فاتورة).
  2. انعكاس اتجاه النصوص: تفتقر المحركات القديمة إلى التطبيق السليم لخوارزمية الاتجاهين الموحدة (Unicode Bidirectional Algorithm)، مما يؤدي إلى عكس اتجاه الجمل العربية وقراءتها من اليسار إلى اليمين بطريقة مشوهة.
  3. تداخل النصوص الهجينة وعلامات الترقيم (BiDi Collisions): عندما تختلط الكلمات العربية بأكواد المنتجات اللاتينية (SKU)، أو أرقام الهواتف، أو الرموز النقدية والنسب المئوية (مثل SYP أو USD أو %)، تقفز علامات الترقيم والأقواس إلى الجانب الخاطئ من السطر وتتشوه بنية الأرقام.
  4. تكسر الجداول عند الانتقال بين الصفحات: عند توليد فواتير طويلة تمتد على عدة صفحات، غالباً ما تنقسم أسطر الجداول في منتصف النصوص، أو يختفي ترويسة الجدول في الصفحات التالية.

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

يقدم هذا الدليل خطة هندسية متكاملة وقابلة للتطبيق الفوري لبناء محرك توليد فواتير PDF باللغة العربية باستخدام تقنيات الويب الحديثة، وقواعد طباعة CSS Paged Media، والتحكم بالمتصفحات الخفية (Headless Chromium).

---

المقارنة المعمارية: المكتبات البرمجية المباشرة مقابل محركات المتصفح

تاريخياً، واجه مهندسو الواجهات الخلفية (Backend) مفاضلة صعبة بين مكتبات توليد الـ PDF المباشرة وبين محركات العرض الخفية:

| معيار المقارنة | مكتبات الفكتور المباشرة (مثل PDFKit وTCPDF وReportLab) | أدوات HTML-to-PDF القديمة (مثل wkhtmltopdf) | محرك Chromium الخفي الحديث (Puppeteer / Playwright) | | :--- | :--- | :--- | :--- | | تشكيل الخطوط العربية (HarfBuzz) | تتطلب إعداداً يدوياً لطبقات OpenType المعقدة ومكتبات C خارجية. | غير مستقرة؛ تعتمد على نسخ قديمة من WebKit تفتقر لدعم معايير التشكيل الحديثة. | تشكيل تلقائي واحترافي مدعوم بمحركي HarfBuzz وSkia الأصليين في كروميوم. | | مرونة التخطيط واتجاه RTL | حساب يدوي معقد لإحداثيات العناصر (doc.text(x, y)). | دعم محدود لـ HTML4 القديم؛ تفشل أنظمة Flexbox وCSS Grid. | دعم كامل وقوي لأنظمة التخطيط الحديثة (Flexbox, Grid) والخصائص المنطقية (CSS Logical Properties). | | تقسيم الصفحات المتعددة | إدارة يدوية لمؤشر الصفحة وحسابات دقيقة لحجم الهوامش. | تقسيم غير دقيق للصفحات يؤدي غالباً إلى قطع النصوص وأسطر الفاتورة. | دعم مدمج لمعايير الطباعة المتقدمة (@page وbreak-inside: avoid وتكرار <thead>). | | استهلاك الذاكرة والمعالج | غالباً أقل، لكنه يعتمد على المكتبة والمستند. | يختلف بحسب المحرك والحمل. | غالباً أعلى؛ قِس الذروة وحدد عدد المهام المتزامنة. |

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

يوفر استخدام Chromium الخفي عبر مكتبة Puppeteer أو Playwright الخيار الأمثل، لأنه يعالج النصوص والخطوط بنفس المحرك البرمجي المتطور الذي تعتمد عليه متصفحات الويب العالمية.

---

الخطوة الأولى: بناء قالب HTML وCSS مخصص للطباعة بالاتجاه العربي (RTL)

يكمن السر في الحصول على مستندات متناسقة في تصميم القالب وفق معايير CSS Paged Media Module المخصصة للطباعة.

إليك هيكل القالب الهندسي المعتمد لإنتاج الفواتير العربية:

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
  <meta charset="UTF-8">
  <title>فاتورة ضريبية</title>
  <style>
    /* 1. تضمين الخطوط محلياً لضمان سرعة التحميل واستقرار التشكيل */
    @font-face {
      font-family: 'Cairo';
      font-style: normal;
      font-weight: 400;
      src: url('data:font/woff2;base64,REPLACE_WITH_FONT_DATA') format('woff2');
    }
    @font-face {
      font-family: 'Cairo';
      font-style: normal;
      font-weight: 700;
      src: url('data:font/woff2;base64,REPLACE_WITH_FONT_DATA') format('woff2');
    }

    /* 2. ضبط أبعاد وهوامش الصفحة للطباعة الرسمية */
    @page {
      size: A4 portrait;
      margin: 15mm 12mm 20mm 12mm; /* علوي، أيمن، سفلي، أيسر */
      @bottom-left {
        content: "صفحة " counter(page) " من " counter(pages);
        font-family: 'Cairo', sans-serif;
        font-size: 9pt;
        color: #64748b;
      }
      @bottom-right {
        content: "شركة دراجون سوفت - نظام الفوترة الرقمي الموحد";
        font-family: 'Cairo', sans-serif;
        font-size: 9pt;
        color: #64748b;
      }
    }

    /* 3. الخصائص العامة لطباعة المستند */
    body {
      font-family: 'Cairo', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      font-size: 11pt;
      line-height: 1.5;
      color: #0f172a;
      direction: rtl;
      text-align: right;
      margin: 0;
      padding: 0;
      -webkit-print-color-adjust: exact;
      print-color-adjust: exact;
    }

    /* 4. قواعد تنظيم الجداول وانتقال الصفحات */
    table {
      width: 100%;
      border-collapse: collapse;
      page-break-inside: auto;
    }

    /* تكرار ترويسة الجدول تلقائياً في كل صفحة */
    thead {
      display: table-header-group;
    }

    /* منع انقسام السطر الواحد عبر صفحتين أبداً */
    tr {
      page-break-inside: avoid;
      break-inside: avoid;
    }

    /* منع فصل مربعات المجاميع والتوقيعات */
    .invoice-summary, .signature-block {
      page-break-inside: avoid;
      break-inside: avoid;
    }

    /* شريط الترويسة مع لمسة لونية مميزة */
    .header-bar {
      border-top: 4px solid #ff003c;
      padding-top: 10px;
      margin-bottom: 20px;
    }

    /* عزل الأكواد اللاتينية والأرقام لتفادي تداخل النصوص */
    .sku-code, .unit-num {
      direction: ltr;
      display: inline-block;
      unicode-bidi: isolate;
    }
  </style>
</head>
<body>
  <div class="header-bar">
    <table style="border: none;">
      <tr>
        <td style="text-align: right;">
          <h1 style="margin: 0; font-size: 20pt; color: #0f172a;">فاتورة مبيعات ضريبية</h1>
          <p style="margin: 4px 0; color: #64748b;">رقم الفاتورة: <bdi>INV-2026-0842</bdi></p>
        </td>
        <td style="text-align: left; vertical-align: top;">
          <span style="background: #fef2f2; color: #ff003c; padding: 4px 12px; border-radius: 4px; font-weight: bold; font-size: 9pt;">
            معتمدة إلكترونياً
          </span>
        </td>
      </tr>
    </table>
  </div>

  <!-- جدول بنود الفاتورة -->
  <table class="items-table">
    <thead>
      <tr style="background: #f1f5f9;">
        <th style="padding: 8px; text-align: right;">البند والوصف</th>
        <th style="padding: 8px; text-align: center;">الرمز (SKU)</th>
        <th style="padding: 8px; text-align: center;">الكمية</th>
        <th style="padding: 8px; text-align: left;">سعر الوحدة</th>
        <th style="padding: 8px; text-align: left;">الإجمالي</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="padding: 10px 8px; border-bottom: 1px solid #e2e8f0;">
          لوحة تحكم إلكترونية صناعية
        </td>
        <td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #e2e8f0;">
          <span class="sku-code">CTRL-992-SY</span>
        </td>
        <td style="padding: 10px 8px; text-align: center; border-bottom: 1px solid #e2e8f0;">04</td>
        <td style="padding: 10px 8px; text-align: left; border-bottom: 1px solid #e2e8f0;">
          <span class="unit-num">150,000 SYP</span>
        </td>
        <td style="padding: 10px 8px; text-align: left; border-bottom: 1px solid #e2e8f0; font-weight: bold;">
          <span class="unit-num">600,000 SYP</span>
        </td>
      </tr>
    </tbody>
  </table>
</body>
</html>

استراتيجيات معالجة النصوص ثنائية الاتجاه (BiDi)

  1. استخدام وسم <bdi> (Bidirectional Isolation): قم بإحاطة كافة الرموز المتغيرة التي تجمع بين الأرقام والحروف والرموز الخاصة (مثل أرقام الفواتير #INV-2026، وأرقام الهواتف +963 991 039 132) داخل وسم <bdi>. يضمن هذا عدم تأثر الرمز بسياق النص العربي المحيط ومنع ارتداد علامات الجمع أو الطرح إلى الجانب المعاكس.
  2. تضمين الخطوط محلياً: تجنب تحميل الخطوط من شبكات CDN الخارجية أثناء تشغيل السيرفر؛ حيث يؤدي أي بطء في الشبكة إلى تأخير بناء الملف أو الاعتماد على خطوط النظام الافتراضية المشوهة. قم بتخزين ملفات الخطوط بصيغة WOFF2 أو TTF محلياً على الخادم.
  3. تثبيت الألوان والخلفيات: تقوم محركات Chromium افتراضياً بحجب ألوان الخلفيات لتوفير الحبر عند الطباعة. إضافة خاصيتي -webkit-print-color-adjust: exact وprint-color-adjust: exact تجبر المحرك على إظهار ألوان الهوية البصرية، وظلال الأسطر، والشارات بدقة.

---

الخطوة الثانية: بناء محرك المعالجة في Node.js

بعد تجهيز القالب، نقوم بإنشاء خدمة برمجية عبر Node.js وTypeScript لتجميع البيانات واستدعاء محرك Chromium لتوليد ملف الـ PDF:

// pdf-generator.ts
import puppeteer, { Browser } from 'puppeteer';
import fs from 'node:fs/promises';

export interface InvoiceItem {
  description: string;
  sku: string;
  quantity: number;
  unitPrice: number;
  total: number;
}

export interface InvoiceData {
  invoiceNumber: string;
  issueDate: string;
  clientName: string;
  items: InvoiceItem[];
  subtotal: number;
  tax: number;
  grandTotal: number;
  qrCodeSvg: string; // كود الاستجابة السريعة للتحقق الإلكتروني
}

export async function generateInvoicePdf(
  htmlContent: string,
  outputPath?: string
): Promise<Buffer> {
  // اترك عزل Chromium مفعلاً وشغّل الخدمة بحساب غير root.
  const browser: Browser = await puppeteer.launch({
    headless: true,
  });

  try {
    const page = await browser.newPage();

    // 1. تفعيل وضع وسائط الطباعة لقراءة قواعد @page
    await page.emulateMediaType('print');

    // 2. ضمّن الخطوط والأصول داخل HTML، ثم انتظر تحميل الخطوط.
    await page.setContent(htmlContent, {
      waitUntil: 'networkidle0',
      timeout: 15000,
    });
    await page.evaluate(() => document.fonts.ready);

    // 3. تصدير مصفوفة الـ PDF بأبعاد الطباعة الصارمة
    const pdfBuffer = await page.pdf({
      format: 'A4',
      printBackground: true,
      preferCSSPageSize: true, // يضمن احترام الهوامش المحددة في CSS بدقة
      margin: {
        top: '15mm',
        right: '12mm',
        bottom: '20mm',
        left: '12mm',
      },
    });

    if (outputPath) {
      await fs.writeFile(outputPath, pdfBuffer);
    }

    return Buffer.from(pdfBuffer);
  } finally {
    await browser.close();
  }
}

---

الخطوة الثالثة: إدارة الذاكرة وتفادي توقف الخوادم (Browser Worker Pool)

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

أعد استخدام المتصفح وحدّد عدد المهام المتزامنة عبر طابور محدود. المثال التالي يوضح إعادة الاستخدام وعزل السياقات، لكنه ليس طابوراً مكتملاً ولا يضمن سقفاً ثابتاً للذاكرة. في الإنتاج، زامن تشغيل المتصفح، وضع إنشاء السياق ضمن معالجة التنظيف، وطبّق حداً للتزامن قبل قبول المهام:

// browser-pool.ts
import puppeteer, { Browser, BrowserContext } from 'puppeteer';

class BrowserPool {
  private browser: Browser | null = null;
  private activeJobs = 0;
  private jobCount = 0;
  private readonly MAX_JOBS_BEFORE_RESTART = 150; // إعادة تشغيل دورية لمنع أي تراكم غير مرئي في الذاكرة

  private async getBrowser(): Promise<Browser> {
    if (!this.browser || !this.browser.connected) {
      this.browser = await puppeteer.launch({
        headless: true,
      });
      this.jobCount = 0;
    }
    return this.browser;
  }

  public async renderPdf(html: string): Promise<Buffer> {
    const browser = await this.getBrowser();
    this.activeJobs++;
    this.jobCount++;

    // إنشاء سياق تصفح معزول (Incognito Context) بدلاً من تشغيل متصفح جديد كلياً
    const context: BrowserContext = await browser.createBrowserContext();
    const page = await context.newPage();

    try {
      await page.emulateMediaType('print');
      await page.setContent(html, { waitUntil: 'networkidle0', timeout: 10000 });

      const pdf = await page.pdf({
        format: 'A4',
        printBackground: true,
        preferCSSPageSize: true,
      });

      return Buffer.from(pdf);
    } finally {
      await page.close();
      await context.close();
      this.activeJobs--;

      // أعد التشغيل دورياً، وراقب الذاكرة وحدد التزامن بشكل مستقل.
      if (this.jobCount >= this.MAX_JOBS_BEFORE_RESTART && this.activeJobs === 0) {
        await this.browser?.close();
        this.browser = null;
      }
    }
  }
}

export const pdfPool = new BrowserPool();

الفوائد الهندسية لهذا النموذج:

  1. إعادة استخدام المتصفح: السياقات المنفصلة تجنّب تشغيل متصفح كامل لكل مستند؛ قِس الفارق على خادمك الفعلي.
  2. ضبط الموارد: تحتاج إلى طابور مهام محدود ومراقبة الذاكرة؛ إعادة التشغيل الدورية وحدها لا تمنع نفادها.
  3. عزل السياقات: يقلل فصل السياقات مشاركة الحالة بين المهام، لكن حماية البيانات الحساسة في السجلات والقوالب والتخزين تبقى ضرورية.

---

الخطوة الرابعة: قائمة التحقق لمطابقة معايير الإنتاج

قبل إطلاق محرك الفوترة في البيئة الحية، احرص على مراجعة المستندات الناتجة وفق قائمة الفحص الهندسية التالية:

| معيار الجودة | سيناريو الاختبار | النتيجة المتوقعة | | :--- | :--- | :--- | | استقرار علامات الترقيم | جملة تنتهي بنقطتين أو أقواس مثل: (المجموع الإجمالي): | تبقى النقطتان والأقواس في الطرف الأيمن/الأيسر الطبيعي وفق مسار RTL دون ارتداد. | | النصوص الهجينة والرموز اللاتينية | اسم عربي متبوع برمز طراز أجنبي: محرك كهربائي صناعي AC-220V | يقرأ رمز الطراز من اليسار إلى اليمين بوضوح دون عكس الأرقام أو تشويه الكلمات المجاورة. | | انتقال الصفحات المتعددة | فاتورة تحوي 45 بنداً وتمتد على 3 صفحات متتالية. | عدم انقسام أي سطر في منتصف النص، مع تكرار ترويسة الجدول بانتظام في بداية كل صفحة. | | دقة الفيكتور (Vector Crispness) | تكبير ملف الـ PDF بنسبة 400% على شاشات الجوال والحاسوب. | تظل الخطوط والأطر وأكواد الـ QR نقية تماماً كرسومات فيكتور دون أي تشويش (Pixelation). | | حجم الملف النهائي | حجم ملف فاتورة من صفحتين. | قِس الناتج على مستندات ممثلة، وحسّن الخطوط والصور المضمّنة عند الحاجة. |

---

الخاتمة: بناء بنية رقمية متينة لأعمالك

إن أتمتة الوثائق المالية باللغة العربية لا تعني التنازل عن وضوح التصميم أو استقرار الخدمة. تساعد قواعد CSS Paged Media وإدارة Chromium بعناية في بناء مستندات فوترة واضحة، مع ضرورة مراجعة المتطلبات القانونية والضريبية لكل حالة على حدة.

سواء كنت تعمل على تطوير نظام ERP شامل لمصنعك، أو أتمتة سلاسل التوريد والتوزيع، أو بناء منصة تجارة رقمية متكاملة، فإن جودة البنية البرمجية هي أساس النجاح واستدامة الأعمال.

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