مقال تقني

تدرجات PDF المحورية والقطرية داخل Delphi مع PDFium

ينشئ PDFium Component تدرجات PDF المحورية والقطرية داخل Delphi عبر TPdf.SaveAsShadingCreated. تصف كل تدرج في سجل TPdfShadingCreate‏ (النوع وفضاء الألوان والإحداثيات والألوان أو المحطات وصفحة الهدف)، وتكتب الطريقة نسخة متحقق منها يكون كل تعبئة فيها قاموس تظليل حقيقي من ISO 32000-1، مسجلاً في قاموس /Resources /Shading للصفحة ومرسوماً بمعامل sh، فتعرضه PDFium أصلياً

تظهر الحاجة في مولدات التقارير والشهادات. يسلم المصمم نموذجاً بشريط ترويسة أزرق إلى أبيض ناعم أو توهجاً قطرياً خلف ختم، ولا شيء يقدمه API كائنات الصفحة الذي يغطي النص والمسارات والصور لأجله. تستطيع انتحال تدرج بمئة مستطيل رفيع بألوان متباينة قليلاً، لكن النتيجة تشرائط بوضوح عند التكبير وتنفخ تدفق المحتوى وتُطبع بشكل سيئ. تملك PDF تظليلات ناعمة منذ 1.3؛ والعمل هو كتابتها بحيث يرسمها عارض فعلاً

لماذا يكتب PDFium Component التدرجات حفظاً منفصلاً؟

TPdf.SaveAsShadingCreated عملية حفظ لا استدعاء كائنات صفحة، لأن الـ ABI العام في PDFium بلا دالة تنشئ قاموس تظليل أو تبث معامل sh. طبقة كائنات الصفحة المستخدمة عند إنشاء ملفات PDF من الصفر بواسطة PDFium Component تبني المسارات والنص والصور، وهناك تتوقف. التظليلات تسكن درجة أدنى، في مخطط الكائنات وقواعد تدفق المحتوى

لذا تعمل الطريقة بمراحل. يحفظ PDFium أولاً المستند المفتوح أصلياً، محافظاً على كل تحرير أجريته عبر الـ API العادي. ثم يعيد PDFium Component كتابة ذلك المخرج المرصوص: يخصص كائناً غير مباشر واحداً لكل تظليل، ويضيف مدخل المورد إلى كل صفحة هدف، ويلحق تدفق محتوى جديداً إلى /Contents للصفحة، ويكتب جدول مراجع متقاطعة جديداً. وقبل أن يصل أي شيء إلى تدفقك أو ملفك يعاد تحميل النتيجة بفحص مراجع متقاطعة صارم، وتعيد الطريقة False مع رسالة في التقرير إن فشل ذلك التحميل. تكتب الصيغة الزائدة بالملف أولاً إلى ملف مؤقت، فحفظ فاشل لا يطمس هدفاً قائماً

بناء تدرج محوري بلونين

التظليل المحوري‏ (ISO 32000-1 §8.7.4.5.3، النوع 2) يمزج الألوان على طول خط من x0 y0 إلى x1 y1، فتأخذ Coords أربع قيم. والتظليل القطري‏ (§8.7.4.5.4، النوع 3) يمزج بين دائرتين ويأخذ ستاً: x0 y0 r0 x1 y1 r1. مع StartColor و EndColor عاديين يبث PDFium Component دالة أسية من النوع 2‏ (§7.10.3) بـ /N 1، وهو استيفاء خطي مستقيم، ويضبط /Extend [true true] فتستمر ألوان الطرفين بعد طرفي المحور

هندسة التظليل المحوري والقطري في PDFium Component لـ Delphi: يمزج تدرج النوع 2 من StartColor إلى EndColor على طول الخط من x0 y0 إلى x1 y1 مع ‏/Extend ‏true true مستمرة بعد الطرفين، بينما يمزج تدرج النوع 3 بين دائرة بداية نصف قطرها r0 ودائرة نهاية نصف قطرها r1
يرسم PDFium التظليل فوق ما يسمح به القص: تضع Coords للنوع 2 محور المزج من x0 y0 إلى x1 y1، وتضع Coords للنوع 3 الدائرتين، وتحمل /Extend [true true] لوني الطرفين بعد الهندسة
uses
  System.SysUtils, PDFium, FPdfCompress;

procedure AddHeaderBand(const InFile, OutFile: string);
var
  Pdf: TPdf;
  Fills: TPdfShadingCreateShadings;
  Report: TPdfShadingCreateReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InFile;
    Pdf.Active := True;

    SetLength(Fills, 1);
    Fills[0] := Default(TPdfShadingCreate);
    Fills[0].ShadingType := pstAxial;
    Fills[0].ColorSpace := pscDeviceRGB;
    Fills[0].Coords := [0, 792, 0, 640];        // ‏x0 y0 x1 y1، من الأعلى إلى الأسفل
    Fills[0].StartColor := [0.12, 0.35, 0.75];  // أزرق داكن عند الحافة العليا
    Fills[0].EndColor := [1, 1, 1];             // أبيض في الأسفل
    Fills[0].PageNumber := 1;                   // تبدأ من 1
    Fills[0].ResourceName := 'HeaderBand';      // اختياري، الافتراضي Sh1 و Sh2...

    if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
      raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
  finally
    Pdf.Free;
  end;
end;

يجب أن يطابق عدد مكونات كل لون فضاء الألوان: واحد لـ pscDeviceGray و pscCalGray، وثلاثة لـ pscDeviceRGB و pscCalRGB و pscLab، وأربعة لـ pscDeviceCMYK. ولـ pscICCBased يأتي العدد من بايتات الملف التعريفي التي تمررها في IccProfile، التي يجب أن تكشف 1 أو 3 أو 4 مكونات؛ ويغرز الملف التعريفي تدفق ICC خاصاً به مع /N المطابقة. وتقبل الفضاءات المعايرة CalWhitePoint اختيارياً (ثلاث قيم، Y موجب)، و CalGamma‏ (قيمة واحدة لـ CalGray وثلاث لـ CalRGB) و، لـ Lab، ‏LabRange من ست قيم. ويُفحص كل إحداثي ومكون من NaN واللانهاية قبل كتابة أي بايت

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

تدرجات متعددة المحطات تُترجم إلى دالة ربط من النوع 3

يحول PDFium Component مصفوفة Stops إلى الدالة التي يتوقعها قارئ PDF: محطتان تصيران دالة واحدة من النوع 2، ومن 3 إلى 256 محطة تصير دالة ربط من النوع 3‏ (ISO 32000-1 §7.10.4) مكونة من مقطع خطي واحد من النوع 2 لكل زوج متجاور. ويحمل كل TPdfShadingCreateStop ‏Position و Color. وعندما تكون Stops غير فارغة تحل محل StartColor و EndColor كليهما

تحتاج دالة الربط ثلاث مصفوفات يسهل الخطأ فيها بخفة يدوياً. تحمل /Functions المقاطع، وتحمل /Bounds مواضع المحطات الداخلية فقط (k مقاطع تحتاج k-1 حداً)، وتعيد /Encode كل نطاق فرعي إلى [0 1] فيرى كل مقطع مدى مدخله الكامل. تشتق المكوّن الثلاثة من محطاتك، ولهذا صارم في المدخل: يجب أن تكون المواضع منتهية، متزايدة تماماً، تبدأ عند 0 وتنتهي عند 1‏ (في حدود 1e-6). قائمة محطات تبدأ عند 0.1 ستترك جزءاً من النطاق دون دالة معرفة، وترفضها الطريقة بدل التخمين

function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
  I: Integer;
begin
  Result.Position := Position;
  SetLength(Result.Color, Length(Rgb));
  for I := 0 to High(Rgb) do
    Result.Color[I] := Rgb[I];
end;

procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
  Fill := Default(TPdfShadingCreate);
  Fill.ShadingType := pstRadial;
  Fill.ColorSpace := pscDeviceRGB;
  // ‏x0 y0 r0 x1 y1 r1: نقطة في مركز الختم تنمو إلى دائرة 90 pt
  Fill.Coords := [450, 160, 0, 450, 160, 90];
  Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
                 Stop(0.4, [0.95, 0.75, 0.30]),
                 Stop(1.0, [1.00, 1.00, 1.00])];
  Fill.PageNumber := 1;
end;

التدرج ذو الثلاث محطات ينتج /Functions بمقطعين من النوع 2، و /Bounds [0.4] و /Encode [0 1 0 1]. حدود المقاطع وصلات لونية صلبة، فقائمة المحطات لا تنعّم عبرها؛ وإن بدت وصلة حادة أكثر من اللازم فأضف محطات بدل توقع القارئ استيفاء انحناء

ترجمة تدرجات متعددة المحطات في PDFium Component داخل Delphi: تصير ثلاث محطات Stops عند المواضع 0.0 و0.4 و1.0 دالة ربط من النوع 3 بمقطع واحد من النوع 2 لكل زوج محطات متجاور، و /Bounds [0.4] للموضع الداخلي و /Encode [0 1 0 1] تعيد كل نطاق فرعي إلى المدى الكامل
كل زوج محطات متجاور يصير مقطعاً واحداً من النوع 2 داخل دالة من النوع 3؛ ومصفوفا /Bounds و /Encode المشتقان هما ما تخطئ فيه دوال الربط المكتوبة يدوياً عادة

لماذا ترسم PDFium شيئاً بصمت؟

لا تعرض PDFium تظليلاً منشأ حديثاً إلا عندما يكون تفصيلان صحيحين: يجب أن يكون /ShadingType عدداً صحيحاً، وأن يجلس استدعاء sh داخل مسار قص. أخطئ في أيهما فيفتح الملف ويتحقق بنيوياً ويعرض صفحة لم تُمس. لا خطأ ولا تحذير ولا سطر سجل

الفخ الأول: ‏/ShadingType عدد لا اسم

يعرف الجدول 78 في ISO 32000-1 خاصية /ShadingType عدداً صحيحاً:‏ 2 للمحوري و3 للقطري. بثت أول نسخة من هذا الكاتب /ShadingType /Axial لأنها قرأت أفضل في تفريغ سداسي. تقرأ PDFium المفتاح بجالب عدد صحيح، فتنال 0 لاسماً، وتصنف التظليل غير صالح، وتفشل في تحميله، وتتخطى معامل sh دون شكوى. عُرضت الصفحة فارغة ولم يقل شيء في PDFium لماذا. لم يصبح السبب مرئياً إلا بعد بناء PDF يدوي مصغر وفتحه في عارض ثانٍ مستقل، أبلغ عن نوع تظليل مجهول يساوي 0. ويتعمم الدرس على كل كاتب PDF: عندما تصف المواصفة تعداداً عدداً صحيحاً، اكتب العدد الصحيح، مهما بدت الاسم الموصف لنفسه مغرياً

الفخ الثاني: تدفق محتوى لا يحوي إلا sh بلا صندوق محيط

معامل sh‏ (ISO 32000-1 §8.7.4.2) يرسم التظليل فوق منطقة القص الجارية، فيحده مدى الصفحة نفسها على الورق. لكن PDFium يبني كائن صفحة تظليل يأتي مستطيله إما من مسار القص النشط وإما من الصندوق المحيط المتراكم لمعاملات الرسم السابقة في التدفق نفسه، وتلغي العارضة الكائنات ذات المستطيل الفارغ. تدفق ملحق حديث لا يحوي سوى /Sh1 sh ليس لهما، فيُهجر التدرج قبل التنقيط. لذلك تقرأ TPdf.SaveAsShadingCreated صندوق القص لكل صفحة‏ (مع اللجوء إلى صندوق الوسائط ثم إلى حجم الصفحة) وتلف كل استدعاء بقص re W n صريح. على صفحة US Letter بأصلها عند الصفر يبدو التدفق الملحق هكذا:

q
0 0 612 792 re W n
/HeaderBand sh
Q

يعزل زوج q/Q القص وأي تحويل عن محتوى الصفحة القائم، فلا يرثهما ما يُرسم لاحقاً على الصفحة. وإن كتبت استدعاءات تظليل بنفسك في أداة أخرى فانسخ هذا الشكل: قص صريح في التدفق نفسه رخيص ويزيل الاعتماد على ما سبقه

لماذا لا يرسم تظليل PDF سليم المظهر شيئاً في PDFium: تقرأ /ShadingType مكتوبةً اسم Axial بجالب عدد صحيح فتكون 0 ويتخطى معامل sh بصمت، وتدفق محتوى لا يحوي سوى sh بلا صندوق محيط حتى يمنحه قص re W n صريح مستطيلاً
فشلا التظليل غير المرئي كلاهما صامت في PDFium، لذا تكتب SaveAsShadingCreated الـ ShadingType عدداً صحيحاً بنفسها وتلف كل استدعاء sh بقص re W n من صندوق القص

كيف يتفاعل مصفوفة تدرج مخصصة مع القص؟

تُطبق مصفوفة التدرج في PDFium Component بوصفها معامل cm يوضع بعد القص وقبل sh، فيبقى القص بكامل الصفحة بينما تتحول هندسة التدرج فقط. اضبط HasMatrix على True واملأ MatrixA حتى MatrixF، الأعداد الستة نفسها لأي مصفوفة تحويل PDF. يجب أن تكون القيم منتهية والمحدد غير صفري، لأن مصفوفة شاذة ستطوي التظليل على خط. يسجل المكوّن أيضاً القيم الست بوصفها /Matrix في قاموس التظليل، لكن الاستدعاء المباشر لـ sh هو ما تضبطه الـ cm في تدفق المحتوى نظامَ إحداثيات يرسم فيه PDFium

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

procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
  BuildSealGlow(Fill);       // تعبئة قطرية مركزة عند 450,160
  Fill.Coords := [0, 0, 0, 0, 0, 90];  // دائرات عند الأصل
  Fill.HasMatrix := True;
  Fill.MatrixA := 1.6;       // مدّ أفقياً
  Fill.MatrixB := 0;
  Fill.MatrixC := 0;
  Fill.MatrixD := 0.8;       // اضغط رأسياً
  Fill.MatrixE := 450;       // ثم انقل إلى مركز الختم
  Fill.MatrixF := 160;
end;

قراءة التقرير ومعرفة الحدود

يخبرك TPdfShadingCreateReport بما كُتب: ‏SourceByteCount و OutputByteCount، و CreatedShadingObjectNumbers‏ (رقم كائن واحد لكل مدخل، بالترتيب)، و AttachedShadingCount و Succeeded و ErrorMessage. تسمي أخطاء التحقق المدخل والحقل، مثل قائمة محطات ليست متزايدة تماماً، فتستحق التسجيل حرفياً

if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
  Writeln(Format('%d shading(s) attached, %d -> %d bytes',
    [Report.AttachedShadingCount, Report.SourceByteCount,
     Report.OutputByteCount]))
else
  Writeln('Rejected: ' + Report.ErrorMessage);

الملف التعريفي محدود عن قصد. ينشئ تظليلات محورية وقطريّة فقط؛ وتُرفض التظليلات الدالية والشبكية (الأنواع 1 و4 إلى 7) وفضاءات ألوان الأنماط. تقبل الدعوة الواحدة من 1 إلى 4096 تعبئة، ويجب أن توجد أرقام الصفحات، وتُقصر أسماء الموارد على 127 محرفاً من الحروف والأرقام والشرطة السفلية والناقصة والنقطة. ويُرفض المدخل المشفر عند مرحلة المقطع الختامي. المستند الموقع يحتاج AllowSignedDocument := True، وبما أن المخرج إعادة كتابة تامة لا تحديث تدريجي فلن تتحقق التواقيع القائمة بعد ذلك، فوقّع بعد إضافة التدرجات. تُرسم كل تعبئة في نهاية محتوى الصفحة فوق ما هو موجود؛ ولمفعول خلفية أنشئ التدرج أولاً وأضف النص والصور بعده، أو استخدم المحتوى الاختياري كما في مقالة التحكم بطبقات المحتوى الاختياري في PDF داخل Delphi عندما ينبغي أن تكون التعبئة قابلة للتبديل

FAQ

هل يدعم SaveAsShadingCreated التدرجات متعددة المحطات؟

نعم. مرر من 2 إلى 256 مدخلاً في Stops بمواضع متزايدة تماماً من 0 إلى 1. محطتان تنتجان دالة من النوع 2، وثلاث أو أكثر تنتج دالة ربط من النوع 3 بمصفوفتي /Bounds و /Encode مشتقتين

لماذا يختفي تدرجي في PDFium والملف يبدو صالحاً؟

تحقق أن /ShadingType هي العدد الصحيح 2 أو 3 لا اسماً، وأن معامل sh يجري داخل قص re W n في تدفق المحتوى نفسه. تتخطى PDFium بصمت أنواع التظليل غير الصالحة وتلغي كائنات التظليل ذات المستطيل الفارغ

أي فضاءات ألوان يمكن للتظليل المنشأ استخدامها؟

يقبل TPdfShadingCreate ‏DeviceGray و DeviceRGB و DeviceCMYK و CalGray و CalRGB و Lab و ICCBased بملف تعريفي من 1 أو 3 أو 4 مكونات. فضاءا Pattern و Indexed ليسا جزءاً من ملف الإنشاء

إنشاء التدرجات والمحتوى الاختياري وبقية الكاتبين المرصوصين متوفرة مع PDFium Component لـ Delphi و C++Builder، الذي يغلّف PDFium لمشاريع Delphi و C++Builder و Lazarus