مقال تقني

إضافة صور JPEG 2000 إلى ملفات PDF في Delphi باستخدام HotPDF

شريحة طبية ممسوحة ضوئيًا، أو قطعة مسح جوي، أو إطار فيلم مؤرشف بكامل النطاق الديناميكي. هذه هي الصور التي تأتي بتنسيق JPEG 2000، وهي تأتي هكذا لسبب معين. يحافظ التنسيق على 12 أو 16 بت لكل قناة، ويضغط باستخدام تحويل المويجات (wavelet transform) بدلاً من كتلة DCT التي يستخدمها تنسيق JPEG، ويمكنه تشفير نفس الصورة إما بضغط بدون فقد (lossless) أو بضغط مع فقد (lossy) من مسار كود واحد. عندما يتعين تحويل مستند مبني من تلك المصادر إلى PDF، يجب أن تمر الصورة عبر مرشح (filter) تخصصه مواصفات PDF لهذا المرمز (codec) تحديدًا

استعاد الإصدار HotPDF v2.228.0 محرك فك تشفير JPEG 2000 يعمل لهذا المسار. كان إصدار سابق قد أرسل الوحدة مع دوال وهمية (stub) تُرجع nil، لذلك كانت واجهة برمجة التطبيقات (API) موجودة لكنها لم تفك تشفير أي شيء. يربط المحرك الحالي OpenJPEG 2.5.4 بشكل ثابت (statically) ويحول مصدر JP2 أو J2K إلى بكسلات يمكن لـ HotPDF وضعها على الصفحة

مرشح JPXDecode في PDF

تحدد المواصفة ISO 32000-1 مرشح JPXDecode في القسم §7.4.9. يقوم كائن الصورة في PDF (XObject) بتسمية الضغط الخاص به في مُدخل /Filter الخاص بـ stream dictionary، وقيمة JPXDecode هي التي تشير إلى أن بيانات المسار هي مسار كود JPEG 2000 وليست JPEG الأساسية التي يحملها /DCTDecode. المرشح هو ما يسمح لملف PDF باحتواء بيانات صورة مضغوطة بالمويجات (wavelet) مع عمق بت عالي، وهو يقبل كلاً من الوضعين بدون فقد (lossless) ومع فقد (lossy) للمرمز، لأن الوضع هو خاصية لمسار الكود نفسه وليس للغلاف المحيط به

تلك النقطة الأخيرة تستحق الاهتمام. تنسيق JPEG 2000 هو خوارزمية واحدة مع حالة خاصة بدون فقد (lossless)، وليس تنسيقين منفصلين. تقوم المويجة 5/3 القابلة للانعكاس بإعادة بناء العينات الأصلية بدقة؛ بينما تقايض المويجة 9/7 غير القابلة للانعكاس تلك الدقة بملف أصغر. يعامل مفكك التشفير كليهما بنفس الطريقة في وقت القراءة، ولهذا السبب يحتاج HotPDF إلى مسار فك تشفير واحد فقط لقبول كل ما يلقيه عليه مسار JPXDecode

ما يفعله مفكك التشفير بالبكسلات

تتوقع كائنات صور PDF (XObjects) في الحالة الشائعة 8 بت لكل مكون في DeviceGray أو DeviceRGB. يتجاوز JPEG 2000 ذلك بشكل روتيني، ونموذج المكونات الخاص به أكثر عمومية من مصفوفة نقطية مجمعة (packed raster)، لذلك لدى مفكك التشفير ثلاث مهام للقيام بها قبل أن تكون البيانات قابلة للاستخدام كصورة عادية

أولاً، يتم إعادة أخذ عينات المكونات ذات عمق البت العالي إلى 8 بت. يتم تصغير العينة ذات 12 بت أو 16 بت إلى النطاق من 0 إلى 255 بحيث تكون النتيجة مصفوفة نقطية عادية من 8 بت. يتم تحويل المكونات الموقعة (signed) إلى نطاق غير موقع (unsigned) أولاً. يهم هذا التفصيل لأنه يعتبر فقداناً في حد ذاته: يفقد المسح الضوئي بالتدرج الرمادي ذو 16 بت نطاق درجاته اللونية العميقة بمجرد أن يصبح صورة PDF بحجم 8 بت، وهي المقايضة الصحيحة للمخرجات على الشاشة والطباعة ولكن ليس لإعادة الأرشفة

ثانياً، يتم تحويل مساحة الألوان YCbCr (يسميها المرمز SYCC) إلى RGB. يخزن JPEG 2000 غالباً اللون في مساحة luma-chroma لكفاءة الضغط، وهي نفس الفكرة التي يستخدمها JPEG الأساسي، ويقوم مفكك التشفير بتطبيق التحويل العكسي القياسي حتى تتلقى الصفحة ألوان RGB حقيقية

ثالثاً، تتم ترقية (upsampling) المكونات ذات العينات الفرعية عن طريق تكرار الجار الأقرب (nearest-neighbor). غالباً ما يتم تخزين قنوات chroma بنصف الدقة، لذلك يقرأ مفكك التشفير كل مكون بأبعاده الخاصة ومعامل أخذ العينات الخاص به، ثم يكرر العينات لرفع كل قناة إلى حجم الصورة الكامل قبل التشذير (interleaving). يبقي تكرار الجار الأقرب هذه الخطوة غير مكلفة؛ نظراً لأن الـ chroma الذي يتم ملؤه كان منخفض التردد في الأساس، فإن التكلفة المرئية تكون صغيرة

صناديق JP2 مقابل مسار كود J2K خام

يأتي ملف JPEG 2000 في شكلين، ويكتشف HotPDF الشكل الذي يقرأه من البايتات الأولى بدلاً من امتداد الملف. ملف JP2 عبارة عن حاوية منظمة في صناديق (box-structured): يفتح بصندوق توقيع مكون من اثني عشر بايت 00 00 00 0C 6A 50 20 20 ويغلف مسار الكود إلى جانب الصناديق التي تصف مساحة الألوان والدقة والبيانات الوصفية. لا يحمل مسار الكود J2K الخام أي حاوية على الإطلاق ويبدأ بعلامة SOC FF 4F FF 51. يقرأ مفكك التشفير تلك البايتات البادئة، ويتعرف على التوقيع، ويختار مرمز OpenJPEG المطابق لكل حالة

يتم التعامل مع كلا الشكلين لأنهما متواجدان عملياً. تصدر أجهزة الالتقاط والأرشيفات التي تحتاج إلى البيانات الوصفية الجانبية ملفات JP2؛ بينما تصدر الأدوات التي تريد أصغر حمولة ممكنة مسار الكود المجرد. يتم تمثيل نوع التنسيق كتعداد (enum)، TJpeg2000FileType، مع الأعضاء jtInvalid، jtJP2، jtJ2K، و jtJPT. يسمي العضو JPT متغير البث JPIP؛ يحلل كاشف توقيع البايت الشكلين اللذين يمكنه فك تشفيرهما، JP2 و J2K، ويبلغ عن أي شيء آخر كـ jtInvalid بحيث يفشل الإدخال غير المدعوم بشكل نظيف بدلاً من إنتاج بيانات غير مفهومة (garbage)

uses
  HPDFJpeg2000;

var
  Decoder: THPDFJpeg2000Decoder;
  Pixels: TJpeg2000ByteArray;
begin
  Decoder := THPDFJpeg2000Decoder.Create;
  try
    if Decoder.LoadFromStream(Input) then          // JP2 or J2K, auto-detected
      if Decoder.GetImageData(Pixels) then
        // Pixels is 8-bit interleaved, ColorComponents channels wide,
        // row-major top to bottom: ready for a DeviceGray/DeviceRGB XObject.
        ProcessRaster(Decoder.Width, Decoder.Height,
                      Decoder.ColorComponents, Pixels);
  finally
    Decoder.Free;
  end;
end;

الضغط بدون فقد (Lossless) ومع فقد (lossy) في جانب التشفير

يقرأ مفكك التشفير كلا الوضعين دون إخباره بأيهما هو. يصبح الاختيار معاملاً فقط عندما تتجه في الاتجاه الآخر وتنتج ملف JPEG 2000، وهو ما يمكن لـ HotPDF القيام به أيضاً من خلال فئة TJpeg2000Bitmap، وهي فئة مشتقة من TBitmap تقوم بتحميل وحفظ البيانات النقطية كملف JP2. تتحكم خاصيتان في المخرجات. LosslessCompression هي قيمة منطقية (boolean) تختار المويجة القابلة للانعكاس عندما تكون صحيحة (true)؛ و CompressionQuality هي TJpeg2000QualityRange، وهو عدد صحيح من 1 إلى 100 حيث 1 صغير ومشوه و 100 كبير ودقيق. تعيش الإعدادات الافتراضية في ثوابت مسماة: Jpeg2000DefaultLosslessCompression هي False و Jpeg2000DefaultLossyQuality هي 80

القرار هو قرار يخص المحتوى. يناسب الضغط بدون فقد (Lossless) نسخة رئيسية، أو مسحاً ضوئياً طبياً أو قانونياً، أو أي شيء قد يتم إعادة تشفيره لاحقاً ويجب ألا يراكم فقداناً جيلياً (generational loss). يناسب الضغط مع فقد (Lossy) بجودة 80 صورة متجهة إلى الشاشة أو الطباعة، حيث يعطي التدهور السلس للمويجة ملفاً أصغر بشكل ملحوظ دون أي تشوهات (artifacts) قد يلاحظها القارئ. هناك تنبيه واحد يخص CMYK يجب الإشارة إليه: تكشف الصورة النقطية عن SetCMYK لتمييز البيانات ذات القنوات الأربع كـ CMYK بدلاً من RGBA، وهو أمر مهم لمسارات الطباعة التي تحافظ على الفواصل اللونية (separations) سليمة

uses
  HPDFJpeg2000;

var
  Bmp: TJpeg2000Bitmap;
begin
  Bmp := TJpeg2000Bitmap.Create;
  try
    Bmp.LoadFromStream(Source);              // decode an existing JP2/J2K
    Bmp.LosslessCompression := True;         // reversible 5/3 wavelet
    // or, for a smaller lossy file:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // matches the default
    Bmp.SaveToStream(Output);                // always writes a JP2 file
  finally
    Bmp.Free;
  end;
end;

لماذا لا توجد سلسلة مرشحات فك تشفير عند التحميل (decode-on-load)

تشكل حقيقة معمارية واحدة كيفية استخدامك لأي من هذا، ومن السهل افتراض العكس. لا يحتوي HotPDF على مرشح صور عام لفك التشفير عند التحميل. عندما تفتح ملف PDF يحتوي بالفعل على صورة JPXDecode، لا يفك المحرك تشفير هذا المسار. بل يحتفظ ببايتات JPEG 2000 تماماً كما هي، بحيث تنقل عملية نسخ صفحة أو دمج مستند الصورة كما هي دون تغيير، بايت ببايت. يمتلك مفكك التشفير نقطة إدخال واحدة، وهي في جانب الإنشاء: الدالة AddImage المستندة إلى الملف، والتي يتم إرسالها بواسطة امتداد الملف للتعامل مع مصادر .jp2 و .j2k و .jpt و .jpc

هذا التقسيم هو التصميم الصحيح وليس قيداً. فك تشفير مسار JPX مضمن عند التحميل، فقط لإعادة تشفيره عند الحفظ، من شأنه أن يحول صورة مؤرشفة بدون فقد إلى صورة مضغوطة مع فقد (lossy) ويضخم كل عملية دمج، كل ذلك من أجل صورة كنت تقصد فقط نقلها من ملف PDF إلى آخر. تمرير المسار حرفياً كما هو يمثل عملية سريعة وبدون فقد للبيانات. يتم تأجيل فك التشفير إلى اللحظة الوحيدة التي يكون فيها مطلوباً حقاً: عندما تسلم المحرك ملف JPEG 2000 من القرص وتطلب منه تنقيط (rasterize) تلك الصورة لوضعها في صفحة جديدة. في تلك اللحظة، يجب أن يتحول الملف إلى بكسلات، ويعمل مفكك التشفير

تسجيل الدعم ووضع صورة

تسجيل صور JPEG 2000 هو خيار اختياري (opt-in) خلف مفتاح الترجمة HPDF_REGISTER_JPEG2000_PICTURE، وهو مغلق افتراضياً. السبب هو تعارض حقيقي، وليس الحذر: يمكن أن يتداخل تسجيل تنسيقات الملفات jp2 و j2k و jpc بشكل عام مع TPicture مع اكتشاف تنسيق BLOB الذي يعتمد عليه TppDBImage في ReportBuilder. قم بتعريف المفتاح عندما لا يكون هذا التكامل قيد التشغيل، ويتم تسجيل تنسيقات الملفات بحيث يتعرف عليها TPicture؛ اتركه غير معرف وسيستمر إرسال امتداد AddImage في فك تشفير ملفات JPEG 2000 مباشرة، لأن هذا المسار لا يمر عبر TPicture على الإطلاق

مع فهم ذلك، فإن وضع صورة JPEG 2000 يتبع نفس إيقاع الاستدعاءات الثلاثة مثل أي صورة أخرى في HotPDF. سلم AddImage مسار .jp2 ونوع ضغط لكيفية تخزين الصورة في المخرجات، ثم حدد موضع فهرس الصورة المرجع على الصفحة باستخدام ShowImage

var
  Pdf: THotPDF;
  ImgIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.AddPage;
    // The .jp2 source is decoded through the OpenJPEG backend, then
    // re-embedded with the compression you request here.
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x, y, width, height in points; final 0 is the rotation angle.
    Pdf.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

الضغط الذي تمرره إلى AddImage يتحكم في كيفية إعادة تخزين الصورة التي تم فك تشفيرها، وليس في كيفية قراءتها. يمكن أن يخرج ملف JPEG 2000 الذي تم فك تشفيره إلى صورة نقطية مرة أخرى كـ DCTDecode JPEG، أو Flate raster، أو أي مرشح مدعوم آخر، أياً كان ما يناسب المستند. يحدث فك التشفير من JP2 أو J2K أولاً بغض النظر عن ذلك، لذا يقبل نفس الاستدعاء مصدراً مضغوطاً بالمويجات ويقوم بتضمينه بأي شكل يتوقعه باقي مسار عملك (pipeline)

للحصول على صورة أوسع لكيفية استقرار الصور والخطوط في المخرجات المنشأة، راجع ملاحظاتنا حول مخرجات التقارير باستخدام الخطوط والصور. عندما يعيد المستند الذي تقوم بتجميعه استخدام محتوى من ملفات PDF الحالية، فإن سلوك المرور المباشر الموضح هنا يقترن بآليات الدمج والمراجعة في تدفقات الكائنات والتحديثات التزايدية. يأتي محرك فك تشفير JPEG 2000 كجزء من مكون HotPDF لـ Delphi و C++Builder، جنباً إلى جنب مع واجهات برمجة تطبيقات الصور والخطوط والمستندات التي تمت تغطيتها في مكان آخر في هذه المدونة