مقال تقني

من WebP إلى PDF في Delphi: داخل مفكك VP8L في HotPDF

يفك HotPDF 2.747.0 صور WebP باستخدام مفكك VP8L مكتوب من الصفر بـObject Pascal، ولذلك تقبل THotPDF.AddImageFromFile مسار .webp مباشرة، من دون DLL لـlibwebp يجب شحنها ومن دون عملية مساعدة يجب تشغيلها. وينفذ المفكك القسم 3 من RFC 9649 كاملًا: اجتياز حاوية RIFF، ورموز البادئة القانونية، ومراجع LZ77 الخلفية، وذاكرة التخزين المؤقت للألوان، والتحويلات العكسية الأربعة كلها. أما إطارات VP8 ذات الفقد فترفض بصوت واضح بدل فكها جزئيًا

كان المحفز عاديًا. تصدر أداة تصميم كل أصل بصيغة WebP لأن هذا هو الإعداد الحديث الافتراضي، ثم تصل الأصول إلى مولد فواتير أو كتالوج ظل يتعامل مع PNG وJPEG عقدًا من الزمن، وفجأة يُرفض نصف الإدخالات. والإصلاح الواضح هو ربط libwebp والمضي قدمًا. لكنه أيضًا الإصلاح الذي يحوّل مكوّن VCL مكتفيًا بذاته إلى شيء له قصة نشر كاملة

لماذا تنفيذ VP8L بدل ربط libwebp؟

تنفذ HotPDF المرماز بـPascal لأن مكوّن Delphi الذي يترجمه العملاء داخل ملفهم التنفيذي لا يستطيع اكتساب DLL وقت تشغيل بصمت. فالتبعية الأصلية تعني تتبع ملف ثنائي 32 بت وآخر 64 بت، وتثبيت إصدار، وشرح سلسلة توقيع الكود لمن ينفذ النشر، وملفًا إضافيًا قد يقرر برنامج مكافحة الفيروسات في محطة مقيدة أنه لا يعجبه. وبالنسبة إلى مكوّن تتمثل ميزته الرئيسية في إسقاطه داخل مشروع والعمل مباشرة، فهذه كلفة حقيقية لا نظرية. والنصف الآخر من الحجة هو أن VP8L صغير: صيغة تجمع بين رموز بادئة وLZ77 مع أربعة تحويلات عكسية وخريطة مسافات جوارية من 120 إدخالًا، والمفكك كله في HPDFWebP.pas أقل من 900 سطر Pascal. وداخل THotPDF.AddImage يقع فرع WebP في موضع توزيع الامتدادات نفسه الذي يوجه .jp2 و.j2k و.jpt و.jpc عبر مسار JPEG 2000، ولذلك كانت التوصيلات موجودة أصلًا، في الموضع نفسه الموصوف في جولة إضافة صور JPEG 2000 إلى PDF في Delphi. ويمكن للمستدعين الذين يريدون بكسلات خام بدل صورة PDF الذهاب مباشرة إلى HPDFDecodeWebPLossless، التي تملأ TWebPCardinalArray بقيم $AARRGGBB بترتيب خطوط المسح

uses
  HPDFDoc, HPDFWebP;

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog.pdf';
    Pdf.BeginDoc;
    // يوجه .webp إلى مفكك VP8L المضمّن، من دون DLL
    Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
    Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

لماذا تُقرأ تدفقات بت VP8L في اتجاهين في الوقت نفسه؟

لأن ترتيب بتات الحاوية وترتيب بتات رمز البادئة محددان بصورة مستقلة، ولأن VP8L يختار اصطلاحين متعاكسين لهما. يذكر القسم 3.2 من RFC 9649 بوضوح أن تدفق البتات يُقرأ من البت الأقل أهمية أولًا: يبدأ القارئ عند البت 0 من البايت ويتجه إلى الأعلى. أما رموز البادئة القانونية الموجودة داخل التدفق فتصل من البت الأكثر أهمية أولًا، من جذر الشجرة، ولذلك تحرك عملية فك الترميز المجمّع إلى اليسار وتضع كل بت جديد في الأسفل باستخدام OR. وهكذا يسير القارئ ومسار الرمز في اتجاهين متعاكسين داخل الحلقة نفسها، وهو ما يبدو كأنه خطأ في كل مرة تعيد فيها قراءته

function TWebPBitReader.ReadBit: Integer;
begin
  if BytePos >= Length(Data) then
    raise EWebPDecode.Create('WebP bitstream exhausted');
  Result := (Data[BytePos] shr BitPos) and 1;   // LSB أولًا، RFC 9649 3.2
  Inc(BitPos);
  if BitPos = 8 then
  begin
    BitPos := 0;
    Inc(BytePos);
  end;
end;

// يسير المسار القانوني في الاتجاه الآخر: أول بت يأتي من التدفق
// هو البت الأكثر أهمية في الرمز
for Len := 1 to 15 do
begin
  Code := (Code shl 1) or BR.ReadBit;
  if Counts[Len] > 0 then
  begin
    if Code - First < Counts[Len] then
      Exit(Symbols[Index + Code - First]);
    First := (First + Counts[Len]) shl 1;
    Index := Index + Counts[Len];
  end
  else
    First := First shl 1;
end;

ثلاثة تفاصيل من RFC تفكك تزامن التدفق بصمت

تُذكر ثلاثة معانٍ في RFC 9649 مرة واحدة بالضبط، ويسهل تجاوزها بالقراءة، وكل واحد منها يكلف بتًا واحدًا أو يوفره، وهو ما يكفي لتحويل كل جدول لاحق إلى ضجيج. وقد ظهرت الثلاثة في مفكك HotPDF لـVP8L، وتنتج جميعها العرض نفسه: صورة تبدو معقولة لكنها خاطئة في كل موضع

  • لا تكتب صورة مشفرة إنتروبيًا في دور غير أساسي بت بادئة وصفية على الإطلاق. فصيغة ABNF لـentropy-coded-image لا تتضمن هذا العنصر ببساطة، ولذلك تؤدي قراءة واحدة إلى فك تزامن التدفق بتًا واحدًا. ويمرر HotPDF القيمة AllowMeta = False لصورة الإنتروبي نفسها، وبيانات تحويل المتنبئ والألوان، ولوحة فهرسة الألوان
  • يستهلك رمز البادئة ذي الورقة الواحدة صفر بتات. يقول القسم 3.7.2.1 من RFC 9649 ذلك مباشرة، وكان المسار القانوني سيقرأ بتًا ثم يفشل في وضعه، ولذلك يكتشف BuildHuff عدد رموز إجماليًا يساوي 1 ويضع الشجرة في حالة Single، ثم يفك ذلك الرمز الواحد من دون لمس القارئ
  • تعني قيمة cache_bits البالغة 0 أن حجم ذاكرة التخزين المؤقت للألوان هو 0، لا 1 shl 0. فالإزاحة المريحة تعطي 1، وتجعل أبجدية الأخضر 256 + 24 + CacheSize تساوي 281 بدل 280، ثم تصبح كل قراءة لجدول رمز بادئة بعد ذلك غير مصطفة
CacheBits := 0;
CacheSize := 0;                        // cache_bits = 0 يعني عدم وجود ذاكرة فعلًا
if BR.ReadBit = 1 then
begin
  CacheBits := Integer(BR.ReadBits(4));
  if (CacheBits < 1) or (CacheBits > 11) then
    raise EWebPDecode.Create('WebP color cache bits out of range');
  CacheSize := 1 shl CacheBits;
end;

// RFC 9649 3.8.3: تحمل صورة ARGB المشفرة مكانيًا فقط
// بت البادئة الوصفية؛ أما أدوار الترميز الإنتروبي فلا تكتبه
if AllowMeta then
  UseMeta := BR.ReadBit
else
  UseMeta := 0;

// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green);   // 280 لا 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);

في fixture المستخدم أثناء الإقلاع ظهرت الثلاثة عند البت 47 ثم 81 ثم 89 بهذا الترتيب. وهذه الأرقام هي مغزى هذا القسم. فلم يعلن أي منها عن نفسه كخطأ بمقدار واحد؛ بل قدم كل واحد صورة اكتمل فكها وبدت كأنها static، وكان الشيء الوحيد الذي فصل بينها موضع البت الدقيق الذي توقف عنده التدفق عن الاتفاق مع المرجع

ماذا يمنحك الفرق حسب موضع البت؟

يحوّل الفرق حسب موضع البت سؤالًا عديم الفائدة إلى سؤال من سطر واحد: ليس لماذا هذه الصورة خاطئة بل لماذا انحرف التدفق عند البت 81. والإعداد رخيص. يكتب Pillow كل fixture بصيغة .webp إضافة إلى تفريغ .rgba لفك الصورة نفسها؛ ويسجل probe بلغة Pascal ونموذج مرجعي صغير بـPython عداد بتات متحركًا بجوار كل قراءة؛ ويكون أول موضع تختلف فيه السجلان هو موضع الخلل. ابدأ بـfixture يمارس أقل قدر ممكن: صورة مسطحة 32x32 لا تحتاج إلا إلى مسار الكود البسيط. اجعلها خضراء، ثم أضف التدرجات والأبعاد الفردية والشفافية، fixture واحدًا في كل مرة. أما تخمين ترتيب البتات بدل ذلك فهو طريقة لقضاء يوم كامل

لكن التحفظ الصادق هو أن المرجع كان خاطئًا أيضًا. فقد نسي نموذج Python قراءة cache_bits، ولم تصل حلقة التحويل لديه إلى نهايتها، ولذلك كانت بعض نقاط الانحراف هي فقدان مفكك المرجع للتزامنه لا مفكك Pascal. ولا يجعل خطأ التنفيذ المرجعي التنفيذ الجاري اختباره صحيحًا، ولا يحصل أي جانب على فائدة الشك: يجب الفصل في كل انحراف بالرجوع إلى نص RFC. وخذ ذلك النص من المصدر أيضًا. فملخصات البحث تشوه الجداول الرقمية كثيرًا، ويجب نسخ خريطة المسافات ذات 120 إدخالًا وأوضاع المتنبئ الـ14 ومضاعف ذاكرة التخزين المؤقت للألوان $1e35a7bd بدقة تامة

أين يختلف تقسيم الأعداد الصحيحة في Pascal عن C؟

تحويل الألوان في VP8L ذو فاصلة ثابتة 3.5 مع دلتا موقعة، وهنا يتوقف Pascal وC عن الاتفاق. إذ تزحزح C الأعداد الصحيحة السالبة حسابيًا، وهو ما يقرّبها إلى الأسفل، بينما يقطع div في Pascal باتجاه الصفر. وفي أي حاصل ضرب سالب يختلف الاثنان بمقدار واحد، ولذلك ينحرف تحويل الألوان العكسي خطوة قناة واحدة لكل بكسل في الصورة كلها. ومن ثم ينفذ HotPDF التقريب إلى الأسفل صراحة في FloorDiv32 بدل الاعتماد على div

// يزحزح C حسابيًا ويقرب السوالب إلى الأسفل، بينما يقطع div في Pascal
// باتجاه الصفر، ولذلك تحتاج الحالة السالبة إلى تصحيح صريح
function FloorDiv32(V: Integer): Integer;
begin
  Result := V div 32;
  if (V < 0) and (V mod 32 <> 0) then
    Dec(Result);
end;

// دلتا فاصلة ثابتة 3.5 بين بايت عنصر تحويل وبايت قناة لون،
// مع تمديد الإشارة لكليهما أولًا
function ColorDelta(T, C: Integer): Integer;
var
  T8, C8: Integer;
begin
  T8 := T;
  if T8 >= 128 then
    Dec(T8, 256);
  C8 := C;
  if C8 >= 128 then
    Dec(C8, 256);
  Result := FloorDiv32(T8 * C8);
end;

يستحق هذا النوع من العيوب تسمية خاصة لأنه غير مرئي في أي اختبار تصادف أن تنتج fixtures فيه حواصل ضرب غير سالبة، حيث يتفق div مع التقريب إلى الأسفل. وهو أيضًا سبب تأكيد اختبارات HotPDF الخاصة بـWebP على مساواة البكسلات حرفيًا مقابل فك Pillow للملفات نفسها بدل سماحية: تدرجات، وحجم فردي 100x37، وصورة 40x40 بقناة ألفا حقيقية، وصورة مسطحة 32x32، كل بكسل يقارن بتًا ببت. فالانحراف بخطوة واحدة يمر في فحص إدراكي ويفشل في فحص بتّي

ما الذي يرفضه دعم WebP عمدًا؟

يفك HotPDF أول chunk من نوع VP8L في ملف WebP ولا شيء آخر. وتعيد إطارات VP8 ذات الفقد، والرسوم المتحركة، وأي حاوية لا يكون chunk المطابق فيها VP8L القيمة False من HPDFDecodeWebPLossless، بينما يحول AddImage ذلك إلى استثناء يسمي الملف: Failed to decode WebP image (lossless VP8L only). وهذا حد مقصود لا سهو، إذ ينبغي لملف ذي صيغة خاطئة أن يفشل حيث يستطيع المستدعي تحويله مسبقًا بدل إنتاج مستطيل رمادي. ويجب أن يكون حقل الإصدار 0، ويُحصر مكدس التحويل في أربعة إدخالات، ويؤدي كل تجاوز للحدود إلى رفع EWebPDecode الذي تحوله نقطة الدخول العامة إلى False عادية. كما أن فك الترميز عند الاستيراد هو الاتجاه المعاكس لسحب الصور من مستند فتحته، وهو ما يمر عبر مسار الصور المحملة الموصوف في استخراج الصور من PDF محمل ومرشحات فكها. وأي مفكك صور هو محلل تغذيه ملفات لم تنشئها: فإذا وصلت أصول WebP من العملاء أو الإنترنت العام، ففحوص الحدود هنا هي الأرضية لا السقف، والإجابة الأقوى هي تشغيل مرمّزات الصور في عملية عاملة معزولة حتى لا يستطيع إطار مشوه إسقاط المضيف معه

والنتيجة العملية هي أن تطبيق Delphi أو C++Builder يستطيع الآن وضع أصول WebP في PDF بالطريقة نفسها التي يضع بها PNG: استدعاء واحد لـAddImageFromFile واستدعاء واحد لـShowImage ولا شيء إضافيًا في المثبّت. وإذا أردت بقية مسار الصور والمستندات المحيط به، فإن مكوّن HotPDF PDF لـDelphi يغطي جوانب الكتابة والتحميل والتصيير من مجموعة الوحدات نفسها