مقال تقني

مرشحات ألوان PDF لضعاف البصر في Delphi باستخدام PDFium

كان طلب الميزة بسيطاً: "الصفحات البيضاء مؤلمة للقراءة؛ أضيفوا dark mode". التنفيذ الأول قلب كل pixel في الصفحة المرسومة، وشُحن خلال أسبوع، ثم ولد تذكرة ثانية خلال أيام: الصور الممسوحة أصبحت تشبه negatives فيلم، وعلامات highlight الصفراء لدى العميل تحولت إلى بقعة زرقاء غير مقروءة، وسأل مستخدم لماذا خرجت الطباعة سوداء. دعم العرض لضعاف البصر في عارض PDF مفيد فعلاً، ومن السهل فعلاً تنفيذه نصف تنفيذ. الفرق بين النتيجتين هو فهم موضع كل قرار لون في pipeline. يستخدم التنفيذ أدناه PDFium Component، مكوّن العارض المبني على PDFium لـ Delphi وC++Builder وLazarus، حيث يعرّض rendering API كل نقطة قرار على حدة.

المرشحات حالة عرض، وليست حالة مستند

القاعدة المعمارية التي تمنع أسوأ فئة من العيوب: نمط القراءة يغيّر طريقة إنتاج bitmap أو معالجتها بعد التصيير، ولا شيء آخر. تبقى bytes الخاصة بـ PDF كما هي، وكل نمط قابل للعكس بإعادة التصيير، و"save" لا يثبت مظهراً مرشحاً داخل الملف. يبدو هذا بديهياً إلى أن يطبع مراجع قانوني عقداً والمرشح نشط ثم يودع النسخة المقلوبة؛ عندها يتضح أن سؤال "هل تستخدم الطباعة مظهر المستند الأصلي أم مظهر الشاشة" يستحق جواباً صريحاً في المواصفة، لا مصادفة في code path. احتفظ بإعداد المرشح في حالة العارض، وطبقه وقت التصيير، واجعل كل مسار export يعلن أي مظهر يستخدم.

تدفع القاعدة ثمنها مرتين. العكسية مجانية؛ تبديل الأنماط يعيد التصيير من المصدر غير المتغير، فلا undo stack ولا طريقة لسلسلة تغييرات أنماط أن تدهور الصفحة. وتبقى سيناريوهات النوافذ المتعددة متماسكة: يمكن لعرضين للمستند نفسه تشغيل نمطين مختلفين، لأن كل view يملك حالة العرض بينما يبقى document object مشتركاً.

صَيِّر أولاً، ثم حوّل

النمط المدعوم هو معالجة bitmap بعد التصيير: ينتج RenderPage raster الصفحة، ثم تعدله مرحلة transform. يشحن المكوّن ثلاث عمليات تحويل، InvertPdfBitmap وDuotonePdfBitmap وGrayscalePdfBitmap، كعمليات in-place على bitmap، ما يجعل تبديل النمط دالة نظيفة من مرحلتين:

function TViewerForm.RenderWithMode(W, H: Integer): TBitmap;
begin
  Result := Pdf.RenderPage(0, 0, W, H, ro0, [reAnnotations]);
  case FReadingMode of
    rmInverted:     InvertPdfBitmap(Result);
    rmHighContrast: DuotonePdfBitmap(Result, clBlack, $0000C8FF);  // dark bg, amber text
    rmGrayscale:    GrayscalePdfBitmap(Result);
  end;
  // rmNormal falls through: the document keeps its own colors
end;

نتيجتان لهذا التصميم تستحقان الاستيعاب. كلفة التحويل تتناسب مع حجم bitmap، لذلك تنتمي إلى موضع تخزين نتائج التصيير مؤقتاً: رشّح bitmap المخزن مرة واحدة، لا في كل paint. ولأن التحويل يعمل على raster النهائي، فهو يطبق بالتساوي على النص والرسوم المتجهة والصور ومظاهر annotations؛ هذا التوحيد هو بالضبط ما يخطئ فيه القلب البسيط مع الصور الفوتوغرافية، ولذلك يكون duotone، الذي يربط luminance بسلم ألوان تختاره من الداكن إلى الفاتح بدلاً من نفي hues، هو الافتراضي الأفضل للمستندات النصية، مع تقديم inversion كاختيار صريح. المستخدمون الذين يطلبون حواف glyph أكثر حدة لديهم ذراع منفصلة: خيار التصيير reNoSmoothText يعطّل text anti-aliasing وقت التصيير ويتوافق جيداً مع high-contrast mode عند zoom كبير.

Grayscaleان لا يتفقان

تتضمن خيارات التصيير reGrayscale، ويبدو كاختصار يتجاوز خطوة post-processing. لكنه ليس العملية نفسها:

// Engine-level: grayscale applied during rasterization
GrayA := Pdf.RenderPage(0, 0, W, H, ro0, [reGrayscale]);

// Post-process: render in color, convert the finished bitmap
GrayB := Pdf.RenderPage(0, 0, W, H);
GrayscalePdfBitmap(GrayB);

الخيار على مستوى المحرك ينطبق على raster output لمحتوى الصور، لكنه لا يصل إلى vector fills أو ألوان النص، ولذلك قد تعود صفحة ذات عناوين ملونة بصور رمادية وعناوين زرقاء عنيدة. يقوم GrayscalePdfBitmap على bitmap النهائي بتحويل كل شيء بلا استثناء. يبقى خيار التصيير مفيداً عندما تريد تحديداً إزالة تشبع الصور مع إبقاء لون النص كإشارة؛ بعض ضعاف البصر يفضلون ذلك بالضبط. لكن إذا كان المتطلب يقول "صفحة grayscale"، فالمعالجة اللاحقة هي النسخة التي تفي به. أياً كان المسار، تذكر أن نمطي overload لـ RenderPage موجودان: الصيغة الدالية تعيد bitmap يملكها المستدعي ويجب تحريرها، وهذا مهم بمجرد أن تضاعف المرشحات عدد bitmaps المرسومة في الذاكرة.

الخلفيات وعلامات التحديد وفخ PageColor

ليست كل تعديلات الراحة transform. استبدال خلفية الصفحة البيضاء بلون دافئ يكفي غالباً لقراء حساسين للوهج، وله خاصية مخصصة، مع قاعدة نطاق تفاجئ الناس:

// Affects the on-screen view only
PdfView.PageColor := $00D9EDF2;  // warm paper tone behind page content

// RenderPage output ignores PageColor; pass the color explicitly
Bmp := Pdf.RenderPage(0, 0, W, H, ro0, [], $00D9EDF2);

تغيّر PageColor ما يعرضه TPdfView، لكن bitmaps المنتجة عبر RenderPage تحتفظ بالأبيض الافتراضي ما لم يحدد معامل Color غير ذلك. العَرَض العملي: الشاشة تعرض الصفحة الملونة، ثم يصدّر المستخدم أو يطبع، فيعود الإخراج إلى الأبيض؛ ضع ذلك تحت قرار سياسة export نفسه من القسم الأول.

خصائص اللون المتبقية، HighlightColor لنتائج البحث، وSelectionColor لتحديد النص، وReadingWordColor لمؤشر الكلمة المنطوقة، تعرّف علامات overlay، ويجب إعادة فحص كل واحدة تحت كل مرشح تقدمه. مؤشر قراءة كهرماني يعمل على الأبيض يختفي بعد inversion؛ وتحديد أزرق باهت يذوب في خلفية high-contrast. حافظ على overlay palettes لكل نمط بدلاً من مجموعة عالمية واحدة، واختبر التركيبات عمداً: المرشحات مع text-to-speech إعداد طبيعي للمستخدمين الذين تخدمهم هذه الميزة، لا حالة حدية. آلية overlay نفسها مغطاة في مقال القارئ القابل للوصول.

الأرقام والتحقق وسؤال الطباعة

يعطي WCAG 2.1 هذه الميزة أهدافاً قابلة للقياس: success criterion 1.4.3 يطلب نسبة contrast قدرها 4.5:1 للنص العادي، و1.4.6 يرفعها إلى 7:1 للتباين المحسن. افحص high-contrast mode لديك ضد هذه النسب بمحلل contrast على الإخراج المرسوم فعلياً؛ النص فوق الصور والنص داخل حقول النماذج هما الموضعان اللذان تفشل فيهما النسب بصمت حتى عندما ينجح متن الصفحة.

بالنسبة إلى الطباعة، يكون الافتراضي القابل للدفاع هو مظهر المستند الأصلي، مع "print as displayed" كاختيار صريح للمستخدم؛ الصفحة المطبوعة دليل في مسارات عمل أكثر مما يتوقعه مؤلفو العوارض، ونسخة مطبوعة مقلوبة من عقد حادثة دعم بطعم قانوني. وبما أن التصيير المرشح يضاعف عمل bitmap عند تبديل الأنماط، فإن استراتيجية التخزين المؤقت في مقال render cache وzoom performance هي القراءة الطبيعية التالية.

FAQ

هل يغيّر dark mode ملف PDF؟

ليس في هذا التصميم؛ التحويلات تعمل على bitmaps مرسومة، وbytes المستند لا تتغير. قل الوعد نفسه في نص UI، لأن المراجعين والمدققين يحتاجون تحديداً إلى معرفة أن ملف المصدر لا تمسه إعدادات العرض.

لماذا تكون الصورة المصدّرة بيضاء بينما تعرض الشاشة صفحة ملونة؟

جاء اللون من PageColor، وهي تؤثر فقط في عرض TPdfView. تمر الصادرات عبر RenderPage الذي يستخدم معامل Color الخاص به؛ مرر اللون هناك، أو اقبل مظهر المستند الافتراضي للصادرات وقل ذلك في UI.

أي نمط ينبغي أن يكون الافتراضي لضعاف البصر؟

قدّم اختيارات بدلاً من انتخاب واحد: high contrast لمعظم القراءة النصية، وinversion لمن يريد تحديداً light-on-dark، وgrayscale لتقليل ضوضاء اللون، وتلوين خلفية لحساسية الوهج. خزّن اختيار كل مستخدم، واستعده عند startup، وأبقِ مساراً بضغطة واحدة للعودة إلى normal.

هل تؤثر المرشحات في أداء التصيير؟

التحويلات تمريرات خطية فوق bitmap النهائي، لذلك تتناسب كلفتها مع عدد pixels لا مع تعقيد المستند، وعند دقات الشاشة تكون أرخص بكثير من التصيير نفسه. التحسين العملي هو تخزين bitmap المرشح وإعادة تشغيل التحويل فقط عندما تتغير الصفحة أو zoom أو النمط؛ لا في كل paint message.

خيارات التصيير، وتحويلات bitmap، وخصائص لون العرض المستخدمة في هذا المقال تُشحن مع PDFium Component لـ Delphi وC++Builder وLazarus/FPC، مع source كامل بحيث يمكن تدقيق أو توسيع تطبيقات التحويل.