إيصال عربي على طابعة حرارية، وخمسة بايتات التهمت الرول كله
لماذا تُفسد طابعات ESC/POS الحروف العربية، وكيف تطبعها صورةً أحادية البت بعرض الرأس الحقيقي، وخلل الأبعاد الذي حوّل إيصالًا طوله 12 سم إلى رولٍ كامل ملقى على الأرض.
كيف أُبقي وحدي نظامًا محاسبيًا من 322 ألف سطر صحيحًا: بوابة حفظ واحدة، وقيود مالية لا تُعدَّل، وسجل تدقيق ببصمات متسلسلة، واختبارات على قواعد حقيقية، وتدقيق كشف ما فاتها.
فوترة محطات الكهرباء دون اتصال: معرّف واحد يرافق الدفعة من الهاتف إلى الإيصال، ولماذا لا تُجمع إجماليات الفواتير، وطابور يعرف ما يعيده، وخطأ 500 صنع حلقة لا تنتهي.
كانت طابعة Xprinter XP-370B على جهاز كاشير يعمل فعلًا، فيها رول بعرض 76 مم، وظلّت تُخرج الورق حتى انتهى الرول. وحين قستُ المهمة لاحقًا دون أن أطبعها، وجدتها تطلب 124.7 مم لا أكثر. المهمة كان لها حدّ، والطابعة لم يكن لها حدّ.
والسبب خمسة بايتات. كل صف في صورة الإيصال كان بعرض 72 بايتًا، على رأس لا يطبع إلا 67.
يطبع FinyX-ERP كل إيصالاته بالعربية، وفي أسفلها رمز QR، على طابعات بدقة 203 نقطة في البوصة، ثمنها قليل ولا تغفر شيئًا. ولا يرسل إليها حرفًا عربيًا واحدًا. وحين تنتهي من هذا المقال ستستطيع أن تطبع عربية سليمة على أي طابعة ESC/POS، بسقف صارم لما تستهلكه من ورق، وستعرف بالضبط كيف تحوّل خمسةُ بايتات البكسلاتِ إلى أوامر.
الوضع النصي في طابعة الإيصالات صُمّم للحرف اللاتيني: خطوط محفوظة في ذاكرتها، وجداول ترميز تقابل كل بايت بشكل واحد. والعربية تطلب أكثر من ذلك بكثير. الحرف الواحد له حتى أربعة أشكال بحسب جيرانه، واللام والألف يندمجان في شكل واحد، والسطر يجري من اليمين إلى اليسار بينما الأرقام داخله تجري من اليسار إلى اليمين. كتبت عن قواعد الاتصال في لماذا تكسر تأثيرات النص حرفًا بحرف العربية. والوضع النصي لا يفعل شيئًا من هذا.
وفي الواقع، جداول الترميز في البرنامج الثابت للطابعة تفصل الحروف، أو تقلب ترتيبها، أو تضع مكانها أشكالًا لا علاقة لها بالعربية، وقد يختلف سلوك جهازين من الطراز نفسه. ولا أستطيع أن أدعم سلوكًا لا أستطيع توقّعه.
لذلك لا تصل العربية في FinyX-ERP إلى أي طابعة نصًّا. الحاسوب يشكّلها بمحرك نصوص حقيقي، والإيصال كله يصير صورة بالأبيض والأسود، والطابعة لا تستقبل إلا صورًا نقطية. وملصقات الباركود على القاعدة نفسها: في طابعات الملصقات بلغة TSPL، يُرسل اسم الصنف العربي صورةً أيضًا.
203
نقطة في البوصة، وبتٌّ واحد لكل نقطة
536
نقطة يطبعها الرأس فعلًا على ورق 76 مم
27 mm
تغذية بعد آخر سطر، تُرسل 216 نقطة
800 mm
أقصى ورق يستهلكه إيصال واحد
الصور تحلّ مشكلة العربية حلًّا كاملًا. لكنها تحمل الخطر الذي تدور حوله هذه القصة، فالصورة عند الطابعة ليست إلا بايتات إضافية في سيل الأوامر.
يركّب QuestPDF الإيصال من اليمين إلى اليسار على صفحة متصلة بلا ارتفاع ثابت، بعرض منطقة الطباعة بالضبط. ثم تُرسم الصفحة بدقة 203 نقطة في البوصة، فيصير كل بكسل نقطة من نقاط الطابعة، ومن هناك إلى آخر الطريق لا شيء إلا البتّات.
تخطيط الإيصالصفحة QuestPDF من اليمين إلى اليسار بعرض 536 بكسل
التحويل إلى صورةPNG بدقة 203 DPI: كل بكسل نقطة طباعة
الفصل إلى أبيض وأسودالحبر تحت 180، والشفافية تُركَّب على الأبيض
الترميزشرائح صورية بأوامر ESC/POS، كل شريحة 128 صفًا
الإرسال الخامعبر طابور الطباعة في ويندوز، بلا إعادة محاولة
النص العربي لا يصل إلى الطابعة نصًّا أبدًا
المُرمِّز قصير، وثلاثة أسطر فيه تقوم بالعمل كله:
csharp
// معادلة Rec. 601 للسطوع، بأعداد صحيحة.var luminance = (77 * red + 150 * green + 29 * blue) >> 8;// يُركَّب البكسل الشفاف على أبيض أولًا. بدون ذلك تُقرأ الخلفية غير المرسومة// سوداء، فيخرج الإيصال مستطيلًا أسود.luminance = (luminance * alpha + 255 * (255 - alpha)) / 255;if (luminance < InkThreshold) // 180 raster[y * rowBytes + x / 8] |= (byte)(0x80 >> (x % 8));
كل نقطة بتّ واحد، وأقصى نقطة يسارًا في البايت هي أعلى بتّاته، فالنقطة x تقع في البايت x / 8 تحت القناع 0x80 >> (x % 8). والصف الذي عرضه 536 نقطة يساوي 67 بايتًا. احفظ هذا الرقم.
الشرائح تخرج بأمر GS v 0، كل شريحة 128 صفًا. هذا الأمر يقدّم الورق بارتفاع الصورة بالضبط، فتلتقي الشرائح بلا خط فاصل، بينما سطر جديد بينها يفتح شريطًا أبيض. وما تحت آخر صف فيه حبر يُحذف إلا هامشًا نحو 2 مم، فلا يخرج بعد الإيصال شبرٌ من الورق الأبيض.
والإرسال عبر winspool مباشرة بنوع بيانات RAW، على أجزاء من 64 كيلوبايت. وتعريف ويندوز المثبّت لهذه الطابعة تعريفُ ملصقات، وRAW يمنعه من تحويل أوامر ESC/POS إلى لغته TSPL. وإن فشل شيء في المنتصف، يستدعي الكود AbortPrinter ولا يعيد المحاولة أبدًا: مهمة ناقصة غامضة تُرسل مرتين قد تكرّر الإيصال أو تُفرغ الرول.
عند 203 نقطة في البوصة، المليمتر نحو ثماني نقاط، لكن عند 203 وحدها. لذلك تتحوّل المليمترات إلى نقاط في دالة واحدة فقط، بدقة الطابعة نفسها، مع تقريب النصف بعيدًا عن الصفر حتى لا تنكمش وحدة الباركود.
الورق
منطقة الطباعة
إزاحة الرأس
58 مم
384 نقطة (48 مم)
0
80 مم
576 نقطة (72 مم)
0
76 مم، على طابعة XP-370B التي جرّبت عليها
536 نقطة (67 مم)
24 نقطة
الأولان ملفّان عامّان. أما أرقام XP-370B فمقيسة: عرض الورق 607 نقاط، والرأس يطبع 536 منها، وأول 24 نقطة لا يصلها ميكانيكيًا. واستعمال العرض كله كان يقصّ العمود الأيمن. وفي الإيصال العربي، الحافة اليمنى هي حيث يبدأ كل سطر، فهي أسوأ حافة يمكن أن تخسرها.
ورق 76 مم607نقطة
ما يستطيع الرأس طباعته عليه536نقطة
ورق 80 مم، المساحة القابلة للطباعة576نقطة
ورق 58 مم، المساحة القابلة للطباعة384نقطة
عرض الورقة وعرض الطباعة رقمان مختلفان (بالنقاط عند 203 DPI)
وفي الإعدادات خمس مناطق طباعة (384 و512 و536 و576 و608 نقطة)، والافتراضي 536 المحافظ. المنطقة الأضيق تُطبع دائمًا. الأعرض هي ما يكسر الأمور، وسترى بعد قليل إلى أي حد.
وأكبر تحسّن في الجودة جاء من قرار واحد. كانت إيصالاتي الأولى تُركَّب بعرض 80 مم، ثم يصغّرها المُرمِّز إلى 536 نقطة، بمعامل 0.84. وتصغير صورة أحادية البت هو ما يجعل العربية مشعّثة: الخطّ المصغَّر يقع بين النقاط، فتضطر العتبة إلى التخمين. الآن تُركَّب الصفحة بعرض منطقة الطباعة نفسها:
csharp
// 536 نقطة عند 203 = 67.07 مم. كل بكسل في الصفحة نقطةٌ مطبوعة.internal static float PageWidthMm(DocumentPrintSettings settings) => (float)(settings.ReceiptPrintAreaDots * 25.4d / RasterDpi);
هذا الرقم 536 جاء من رولٍ ملقى على الأرض، وسنصل إليه. لكن العربية نفسها تحتاج قواعد أولًا، فبتٌّ واحد لكل نقطة وسطٌ قاسٍ لا يرحم.
رأس الطباعة عند 203 لا يعرف الرمادي. كل نقطة إما تحترق أو لا، والخط الأرقّ من نقطة لا يُطبع أصلًا. ومن هنا جاءت أربع قواعد.
اختر الخط بالتجربة. رسمت الفاتورة نفسها بخط Ping AR + LT وبخط Cairo بالمقاس الحقيقي، ومرّرتهما على العتبة نفسها، ثم قارنت. خط Ping أضيق وأكثر انتظامًا، فيتّسع السطر لاسم صنف أطول، وتبقى سيقانه بعرض نقطة كاملة عند 7.5 نقطة. أما Cairo فيبدأ بالتفتّت عند هذه المقاسات. فصار Ping الخط الافتراضي للقوالب الحرارية.
وزن Medium للمتن. عند 8 نقاط، وصلات الوزن العادي المصمَّم للشاشات المضيئة تقلّ عن نقطة واحدة فتنكسر، ويبدو الحرف ممزّقًا.
أرضية 6.5 نقطة. هذا نحو 18 نقطة طباعة ارتفاعًا. وما دونها تبدأ النقاط التي تفرّق الباء عن التاء عن الثاء بالذوبان في جسم الحرف.
لا ألوان ولا رمادي. كل شيء ينهار إلى أبيض أو أسود عند العتبة، فالشريط المظلَّل الفاتح إما لا يُطبع، وإما يصير كتلة سوداء فوق نصّه. واسم المستند نصٌّ أبيض على شريط أسود مصمت.
المتن 8 نقاط، والعناوين الصغيرة 7.5، والإجمالي المستحق 13. ومقاسات بهذا الصغر لا تنجح إلا لأن شيئًا لا يُصغَّر في طريقه إلى الرأس.
مع ثقافة عربية، يحوّل محرك النصوص الأرقام إلى الهندية، فيُطبع 76,400.00 هكذا: ٧٦٬٤٠٠٫٠٠. هذا مقروء، لكن الزبون يقارن السعر بملصق الرف، والمورّد يقرأ أرقام الباركود، وكلاهما يحتاج أرقامًا غربية. لذلك الأرقام الغربية هي الافتراضي، والهندية خيار في الإعدادات. وفي مُصيِّر WPF الذي يرسم الملصقات، المفتاح كائن واحد:
والتاريخ مع الوقت، مثل 2026/09/08 14:35، فيه مقطعان لاتينيان. داخل فقرة عربية يُرتَّبان من اليمين إلى اليسار، فيصل الوقت إلى الورق قبل التاريخ. لذلك توضع هذه القيم داخل عازل اتجاه من اليسار إلى اليمين (isolate):
csharp
// U+2066 LEFT-TO-RIGHT ISOLATE … U+2069 POP DIRECTIONAL ISOLATEinternal static string Ltr(string value) => string.IsNullOrEmpty(value) ? value : $"{value}";
العازل يثبّت ترتيب الحروف ويترك موضعها لتدفّق السطر العربي. أما الحاوية ذات الاتجاه اليساري فتدفع القيمة أيضًا إلى الحافة اليسرى من خليتها.
ولا تضع نصًّا عربيًا داخل العازل أبدًا: هناك تنقلب حروف رمز الريال اليمني «ر.ي». لذلك تُطبع العملة برمزها الدولي (YER وSAR)، مرةً واحدة، على الإجمالي المستحق. وخطآن من العائلة نفسها وصلا إلى الورق: سطر الدفع كان يبني نصّه دون عازل فطُبع YER 75,000.00 مقلوبًا، والرصيد السالب طُبع 50,000.00- لأن إشارة السالب أُضيفت خارج العازل. وكلاهما يمرّ الآن على منسّق واحد.
مشكَّلة ومقيسة ومرتّبة، صارت الإيصالات أخيرًا كما ينبغي. ثم طبع كاشير يعمل فعلًا واحدًا منها، ولم يتوقف.
بدت الفرضية الأولى متماسكة. التعريف يعلن وسائط بعرض 38.1 مم والإيصال بعرض 76 مم، فبدا أن الطابعة عالقة في وضع الملصقات، تبحث عن فاصل لا وجود له في الورق المتصل.
أسقطتُها بسكربت PowerShell. أرسل ESC/POS خامًا إلى الطابعة نفسها بالإعدادات نفسها، فطبع نسخة واحدة سليمة وتوقف. ومهمة RAW تتجاوز التعريف كليًا، فمقاس الوسائط فيه لا يمكن أن يكون السبب. الفرق إذن في البايتات.
فقست المهمة دون أن أرسلها. أداة تشخيص تشغّل المركِّب والمُرمِّز الحقيقيين، وتكتب سيل الأوامر بدل الورق:
text
QuestPDF: 1 page, 639 × 1037 px = 129.8 mmESC @ · ESC a 1 · GS L 0 · GS W 576 · 8 × GS v 0 (72 bytes × 128 rows) · ESC d 3paper requested: 124.7 mm
اثنا عشر سنتيمترًا، ولا بايت واحد زائد. والذي اختلف عن السكربت الناجح هو الأبعاد:
السكربت الذي يعمل
FinyX-ERP قبل الإصلاح
GS L، الإزاحة اليسرى
24 نقطة
0
GS W، منطقة الطباعة
536 نقطة
576
صف الصورة
67 بايتًا = 536 نقطة
72 بايتًا = 576 نقطة
المحاذاة
لا شيء
ESC a 1
النهاية
ESC J 240، تغذية بالنقاط
ESC d 3، تغذية بالأسطر
هذه هي البايتات الخمسة. 576 نقطة بإزاحة صفر أعرض مما يطبعه هذا الرأس على ورق 76 مم. وحين يستقبل البرنامج الثابت صفوفًا أعرض من منطقة طباعته، يعيد المزامنة في منتصف الصورة، ومن تلك اللحظة يقرأ بايتات الصورة على أنها أوامر. وبيانات الصورة مليئة ببايتات معناها «غذِّ الورق». هكذا يصير إيصال طوله 12 سم رولًا على الأرض.
وESC d زاد الطين بلّة: يغذّي بالأسطر، وبعض البرامج الثابتة القادرة على الملصقات تقرؤه طلبًا للانتقال إلى النموذج التالي، والورق المتصل لا نموذج تاليًا فيه.
قبلبعد
GS L 0 و GS W 576GS L 24 و GS W 536
72 بايتًا لكل صف صورة67 بايتًا لكل صف صورة
صفوف أعرض من مساحة الطباعةصفوف بعرض مساحة الطباعة تمامًا
البرنامج الثابت يفقد التزامن ويقرأ بايتات الصورة أوامر تغذيةالإيصال ينتهي عند 134.9 مم
كيف تحوّل إيصال طوله 12 سم إلى رول على الأرض، وكيف أُصلح
الإصلاح ثلاثة أجزاء:
كل صف في الصورة بعرض ما يعلنه GS W تمامًا (rowBytes = printAreaDots / 8)، وما يتجاوزه يُقصّ. فلا يمكن أن يختلف المُرمِّز والطابعة على عدد البايتات، وفحصٌ يشترط rowBytes × 8 == width بالضبط.
ESC J بالنقاط مكان ESC d، مقسّمًا على خطوات لا تتجاوز 255 لأنه يأخذ بايتًا واحدًا.
حُذف ESC a 1. الصورة تملأ المنطقة كلها، فلم تكن المحاذاة إلا طريقًا آخر لاختلاف الطرفين.
بعد الإصلاح صار السيل مطابقًا للسكربت الناجح، أمرًا بأمر:
text
ESC @ · GS L 24 · GS W 536 · 7 × GS v 0 (67 bytes × 128 rows) · ESC J 216paper requested: 134.9 mm
ثم تأكدت أن أي خطأ تخطيط قادم سينتهي برسالة خطأ. الإيصال الواحد لا يتجاوز 800 مم من الورق، بالتغذية، ويُفحص ذلك قبل أن يخرج أول بايت؛ وكان السقف القديم 40,000 صف، أي نحو خمسة أمتار. وفي إعدادات الطباعة زرٌّ يُفرغ قائمة الطابعة، وسكربت يفعل الشيء نفسه دون تشغيل البرنامج:
powershell
# أوقف القائمة حتى لا تبدأ مهمة جديدة، ثم ألغِ ما فيها.Add-Type -AssemblyName System.Printing, ReachFramework$server = New-Object System.Printing.LocalPrintServer$queue = $server.GetPrintQueues() | Where-Object FullName -eq 'Xprinter XP-370B'$queue.Pause(); $queue.Commit()foreach ($job in $queue.GetPrintJobInfoCollection()) { $job.Cancel(); $job.Dispose() }
واعرف حدوده. الورق المتحرك الآن يأتي من ذاكرة الطابعة نفسها ويتوقف حين تنفد. فإن استمر بعد ذلك، فالطابعة تغذّي من تلقاء نفسها، ولن يوقفها إلا مفتاح الطاقة أو تصحيح وضع الوسائط.
وفي سجل الأخطاء نفسه طريقٌ ثانٍ لإهدار رول. Process.Start مع الفعل print يرسل ملف PDF إلى طابعة ويندوز الافتراضية. وعلى جهاز كاشير تكون فيه الطابعة الحرارية هي الافتراضية، تمرّ صفحة A4 عبر التعريف الحراري إلى رول لا حدّ لطول صفحته. الآن يُفتح ملف PDF، ويختار إنسانٌ الطابعة بنفسه.
رمز QR. يُرسم الرمز بصيغة SVG: مربّع لكل وحدة، وshape-rendering="crispEdges"، بلا تنعيم. الرمز النقطي حين يُعاد تحجيمه تتداخل وحداته فيعجز عنه الماسح، والمتّجه يُرسم مباشرة على شبكة الطابعة نفسها.
csharp
var matrix = new QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, 0, 0, new Dictionary<EncodeHintType, object> { [EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M, [EncodeHintType.MARGIN] = 0, // الهامش الهادئ يُضاف يدويًا: 4 وحدات });// ثم مربّع "M x,y h1v1h-1z" لكل وحدة سوداء
مستوى تصحيح الأخطاء M: رمز الإيصال نحو 550 حرفًا، والمستوى H يكبّره حتى لا يُطبع بحجم يُقرأ، وM يحتمل خدشًا أو بقعة حبر عادية. ويُطبع بمقاس 38 × 38 مم، في المنتصف بين الإجماليات والتذييل.
التغذية. بعد آخر سطر يتقدّم الورق 27 مم، فيتجاوز التذييل حافة التمزيق. وESC J يعدّ وحدات حركة، وهي نقطة واحدة في هذه الطابعات، فالـ 27 مم تساوي 216 نقطة.
القطع. فقط إن كان للطابعة قاطع، لأن أمر القطع إلى طابعة بلا قاطع يعلّقها أو يُطبع حروفًا. تعريف XP-370B يعرض None وTear Off وPeel Off، ولا يعرض Cut، فيمزّق الكاشير الورقة بيده. وفي غيرها يقطع GS V قطعًا جزئيًا مرةً واحدة بعد الإيصال كله. والمسار الحالي ما زال يتعرّف على XP-370B من اسمها؛ أما المُرمِّز الجديد المقاد بملف الطابعة، الذي أنتقل إليه، فيقرأ القاطع من ملفها.
الدرج. يرسل ESC p نبضة لدرج النقود. وهو في المُرمِّز الجديد، ومطفأ افتراضيًا: نبضة إلى طابعة موصولة بشيء آخر أحسن أحوالها صوت طقطقة.
قِس منطقة الطباعة على الطابعة الحقيقية، واحفظها لكل طابعة، وأعلنها في كل مهمة بـ GS L وGS W.
اجعل كل صف في الصورة printAreaDots / 8 بايتًا بالضبط، وقُصّ ما يزيد.
ركّب الصفحة بعرضها النهائي. لا تصغّر صورة أحادية البت أبدًا.
ركّب الشفافية على الأبيض قبل العتبة.
غذِّ الورق بالنقاط بأمر ESC J، وضع سقفًا للورق قبل أن ترسل أي شيء.
عايِن البتات التي سيحرقها الرأس. أداة المعاينة عندي تمرّر الصفحات على المُرمِّز الحقيقي وتحفظ النتيجة PNG، وكشفت الالتفاف والانعكاس والطول الزائد قبل أن تكلّف ورقة.
لا تُعِد الطباعة تلقائيًا أبدًا.
حين صارت العربية بكسلات، لم يبقَ من المشكلات إلا الهندسة: بأي عرض تطبع، وكم تغذّي، وأين تتوقف. وخمسة بايتات منها كلّفت رولًا كاملًا.
التعليقات
شارك في النقاش
سجّل الدخول بحساب Google لتعجب بالمقالات وتعلّق عليها. ضغطة واحدة تكفي.