يستورد PDFlibPas، مكتبة PDF لـ Delphi، صور TIFF التي يتجاوز فيها ضغط JPEG حدود الـ baseline ذي 8 بت: التسلسلي الممتد ذا 12 بت (SOF1)، واللافقد Huffman (SOF3)، والمرمَّز حسابيًا (SOF9 وSOF10)، وJPEG القديم بنمط TIFF 6.0 مع إزاحات جداول. ولأن DCTDecode في PDF لا يستطيع حمل تلك المسارات، يفكها PDFlibPas في Pascal الأصلي ويضمّن العينات بلا فقد بدقة 8 أو 16 بت لكل مركب
المجموعة الكاملة تتطلب v3.724.0 أو أحدث، وهي موجَّهة مباشرة إلى TIFFs القادمة من الماسحات وبرامج الالتقاط الطبية والعلمية وأنظمة الأرشفة المتجمدة في التسعينيات. وما يلي: لماذا لا يمكن نسخ تلك الصور إلى PDF كما هي، وكيف يحطها PDFlibPas، وما مصائد Pascal التي وُجدت في الطريق
لماذا لا يستطيع JPEG ذو 12 بت دخول PDF مباشرة؟
لا يمكن تمرير JPEG ذي 12 بت لأن PDF لا يملك أي filter يطلب منه فك ذلك. يعرف ISO 32000-1 §7.4.8 DCTDecode حول صيغة JPEG baseline، مع امتداد progressive المضاف في PDF 1.3. ولا شيء في تلك الفقرة يغطي دقة عينات 12 بت، ولا المسار اللافقد في الملحق H من ITU-T T.81، ولا مرمِّز الإنتروبيا الحسابي في الملحق D. والقارئ الذي يصادف إطار SOF1 ذا 12 بت أو علامة SOF9 داخل stream DCTDecode يحق له ألا يرسم شيئًا، ومعظم viewers تفعل ذلك بالضبط
أما شرائط Huffman baseline وprogressive ذات 8 بت التي تشكل معظم ملفات JPEG داخل TIFF، فلا يزال PDFlibPas يتحقق من كل scan ويمرر البايتات المضغوطة كما هي كـ DCTDecode، وهو ما يبقي الـ PDF صغيرًا. وكل ما عدا ذلك يفك. يقرأ PDFlibPas علامة الإطار ودقة العينات لأول كتلة JPEG، وحين يجد SOF3 أو SOF9 أو SOF10 أو دقة غير 8، تمر الصفحة عبر مفكك العينات بدل مسار التمرير. وكشف الترميز الحسابي من علامة إطاره أهم مما يبدو: ففحص ينظر فقط إلى دقة العينات يسمح لـ TIFF كلاسيكي بتمرير شريط SOF9 ذي 8 بت إلى مسار DCTDecode وإنتاج PDF لا تستطيع القارئات عرضه
ماذا يكتب PDFlibPas في الـ PDF بدلًا
يحول PDFlibPas كتل JPEG المفكوكة إلى image XObject عادي بـ BitsPerComponent من 8 أو 16. وتخزن العينات غير مضغوطة افتراضيًا؛ استدعِ CompressImages(1) قبل إضافة الصورة فيكتبها PDFlibPas بـ FlateDecode. ويسمح ISO 32000-1 §8.9.5.1 بـ 16 بت لكل مركب انطلاقًا من PDF 1.5، فيرفع PDFlibPas إصدار المستند إلى 1.5 على الأقل كلما أضيفت صورة بعينات 16 بت. ويشمل ذلك صور multi-strip وtiled التي كائنها الجذر Form XObject بلا BitsPerComponent خاص به لتفقده
الخريطة ثابتة لكل مسار JPEG:
- JPEG ذو 12 بت Huffman (التسلسلي SOF1 والتدريجي SOF2) يلغى تكميمه ويخضع للتحويل العكسي ويتوسع إلى عينات 16 بت
- JPEG المرمَّز حسابيًا (التسلسلي SOF9 والتدريجي SOF10) يصير عينات 8 بت عند دقة 8 بت وعينات 16 بت عند دقة 12 بت
- JPEG اللافقد Huffman (SOF3) يصير عينات 8 بت أو 16 بت مطابقًا دقته المعلنة تمامًا
التوسع من 12 إلى 16 بت يستخدم مضاعفة البتات، أي (V shl 4) or (V shr 8)، فيقابل 4095 بـ 65535 ويبقى الأبيض أبيض بدل أن يتوقف عند 65520. لا يخفض PDFlibPas أبدًا تلك الصور إلى 8 بت من خلف ظهرك. وعندما يحوّل المصف المكتبة صفحة كهذه لسطح عرضها الخاص تستخدم البايت الأعلى من كل عينة، لكن صورة PDF المضمَّنة تحفظ الستة عشر كلها
الثمن هو حجم الملف: حتى مع CompressImages(1)، عينات 16 بت المضغوطة بـ Flate أكبر من بيانات DCT التي جاءت منها، وبدونه أكبر أكثر. وفي مسارات التشخيص والأرشفة تكون تلك المقايضة صحيحة غالبًا. وإن كان الحجم أهم في مهمة معينة، فيعرض دليل تقليص حجم ملف PDF في Delphi أين تذهب البايتات فعلًا
استيراد تلك TIFFs عبر TPDFlib
كود الاستدعاء لا يتغير إطلاقًا. تأخذ TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer رقم صفحة TIFF بالترقيم من 1 في Options وتعيد معرف صورة، أو 0 حين لا يمكن استيراد الصفحة. وتُرفض صور JPEG غير المدعومة قبل إدخال الصورة، فالـ 0 يعني أن شيئًا مفكوكًا نصف فك لم يصل إلى المستند
uses
PDFlibrary;
function TiffToPdf(const TiffName, PdfName: WideString): Boolean;
var
Lib: TPDFlib;
Page, PageCount, ImageID: Integer;
DPI, W, H: Double;
begin
Result := False;
Lib := TPDFlib.Create;
try
Lib.SetMeasurementUnits(0); // فضاء المستخدم الافتراضي، 1/72 بوصة
Lib.SetOrigin(1); // الأصل أعلى اليسار لـ DrawImage
Lib.CompressImages(1); // Flate للعينات المفكوكة
PageCount := Lib.GetImagePageCount(TiffName);
if PageCount = 0 then
Exit;
for Page := 1 to PageCount do
begin
ImageID := Lib.AddImageFromFile(TiffName, Page); // Options = صفحة TIFF
if ImageID = 0 then
Exit; // بيانات صورة غير مدعومة أو تالفة
Lib.SelectImage(ImageID);
case Lib.ImageResolutionUnits of
2: DPI := Lib.ImageHorizontalResolution; // نقاط في البوصة
3: DPI := Lib.ImageHorizontalResolution * 2.54; // نقاط في السنتيمتر
else
DPI := 0;
end;
if DPI <= 0 then
DPI := 300;
W := Lib.ImageWidth * 72 / DPI;
H := Lib.ImageHeight * 72 / DPI;
if Page > 1 then
Lib.NewPage;
Lib.SetPageDimensions(W, H);
Lib.DrawImage(0, 0, W, H);
end;
Result := Lib.SaveToFile(PdfName) = 1;
finally
Lib.Free;
end;
end;
لا حاجة لأي تفريع بحسب الصيغة، وتتبع AddImageFromStream وAddImageFromString القواعد نفسها مع بيانات TIFF المحفوظة في الذاكرة
JPEG القديم: جداول عند إزاحات، وشرائط بلا ترويسات
منذ v3.713.0 يستورد PDFlibPas صور TIFF ذات JPEG القديم (Compression = 6) التي تأتي جداولها من وسوم الإزاحة JPEGQTables وJPEGDCTables وJPEGACTables أو من stream JPEGInterchangeFormat يحوي جداول فقط. عرّف القسم 22 من TIFF 6.0 ذلك التخطيط، واستبدله لاحقًا TIFF Technical Note 2 بـ Compression = 7 ووسم JPEGTables مشترك، لكن الماسحات واصلت كتابة الصيغة القديمة سنوات
وجهة الحاوية، أي BigTIFF والتخطيطات الـ tiled، مغطاة في التقرير الخاص بتقسية مفكك TIFF؛ والتركيز هنا على حمولة JPEG. كل وسم إزاحة جدول يشير إلى جدول تكميم خام من 64 بايتة، أو إلى جدول Huffman من 16 عدًّا يتبعها قيمه، بإدخال واحد لكل مركب. ويحولها PDFlibPas إلى قطع DQT وDHT داخل stream JPEGTables مصنَّع. وكل شريط من بيانات الإنتروبيا الخالصة يحصل بعدها على ترويسة خاصة به: SOI، وعلامة إطار تحمل عدد صفوف ذلك الشريط، وDRI حين يكون JPEGRestartInterval مضبوطًا، وSOS، وبيانات الإنتروبيا، وEOI. ولا يمكن لصق الشرائط في scan واحد، لأن JPEG القديم يعيد تنبؤ DC عند كل حد شريط
تفصيلان يسهل الخطأ فيهما. مركبات صورة YCbCr الثلاثة كثيرًا ما تشير إزاحات جداولها إلى البايتات نفسها، لذا يزيل PDFlibPas تكرار الجداول بحسب المحتوى قبل إسناد أرقام الجداول. وإن بقي أكثر من جدولي Huffman من صف واحد، فالإطار المصنَّع يستخدم SOF1 بدل SOF0، لأن إطار baseline لا يحق له الإشارة إلا إلى جدولي DC وجدولي AC. وحين يشير JPEGInterchangeFormat بدلًا إلى stream JPEG كامل يحوي scans، يعامله PDFlibPas كالصورة كلها ويستورده شريطًا واحدًا بالنمط الجديد. أما JPEG القديم اللافقد والـ tiled والوحيد المستوى (separate-plane) فيُرفض بخطأ صريح بدل التخمين، فحوّل تلك الحالات إلى JPEG بالنمط الجديد في مصدر البيانات
لماذا يخرج JPEG بألوان CMYK داخل TIFF معكوسًا؟
يخرج JPEG ذو CMYK داخل TIFF معكوسًا حين يطبق المستورد اصطلاح Adobe APP14 الذي يعود لملفات JPEG المستقلة. تكتب Photoshop CMYK معكوسًا في ملفات JPEG المستقلة، فيضيف منتجو PDF تقليديًا /Decode [1 0 1 0 1 0 1 0] للتعويض. ويعرف PhotometricInterpretation 5 في TIFF العينات ككميات حبر يكون فيها 0 بلا حبر، وقارئات TIFF السائدة تأخذ عينات JPEG المفكوكة على ظاهرها دون النظر إلى APP14
يضمّن PDFlibPas شرائط TIFF ذات CMYK المضغوطة بـ JPEG دون مصفوفة Decode المعكوسة، سواء وُجدت علامة APP14 أم لا. ولا يزال وسم التحويل في APP14 يقرر هل يتحول YCCK إلى CMYK، ويتولى DCTDecode تلك الخطوة بنفسه. وتحفظ ملفات JPEG المستقلة ذات المركبات الأربعة اصطلاح Adobe المعكوس، فتُعامل بايتات JPEG المتطابقة اختلافًا بحسب الحاوية، كما تقتضي الصيغتان
لماذا ينكسر فك JPEG اللافقد على إزاحة في Pascal؟
ينكسر فك JPEG اللافقد في Pascal لأن المتنبئين 5 و6 يحتاجان إزاحة يمينية حسابية وshr في Pascal منطقية. يتنبأ SOF3 بكل عينة من جارها الأيسر (Ra) والأعلى (Rb) والأعلى الأيسر (Rc) ويرمز الفرق فقط. ومن المتنبئين السبعة في الجدول H.1 من T.81، المتنبئ 5 هو Ra + ((Rb - Rc) >> 1) والمتنبئ 6 هو Rb + ((Ra - Rc) >> 1)، والفرق داخل القوسين يصير سالبًا كلما كان الجار الأعلى الأيسر هو الأفتح. أعطِ Integer سالبًا إلى shr فيصير بت الإشارة بت قيمة، فتتحلل الصورة إلى ضجيج
استبدال الإزاحة بـ div 2 لا يصلحها أيضًا: div يقطع نحو الصفر بينما الإزاحة الحسابية تقرب نحو اللانهاية السالبة، والاثنان يختلفتان على كل قيمة سالبة فردية. يملك Free Pascal SarLongint في وحدة System؛ وليس لدى Delphi ما يعادله، فيستخدم PDFlibPas تابعًا مساعدًا يترجم بشكل مطابق على الاثنين:
// إزاحة حسابية يمينًا بواحد: أي floor(Value / 2)
// shr إزاحة منطقية، وdiv 2 يقطع نحو الصفر
function HalfDown(Value: Integer): Integer;
begin
if Value >= 0 then
Result := Value shr 1
else
Result := -((1 - Value) shr 1);
end;
// Value = -3: -3 shr 1 = 2147483646، -3 div 2 = -1، HalfDown(-3) = -2
أما وجه عرض الأنواع من الصنف نفسه من المشكلات، فانظر مصائد Longint والمؤشرات على Delphi Linux64
لبقية مفكك اللافقد قواعد حدود خاصة به. الصف الأول، وأول صف بعد كل علامة restart، يتنبأ من الجار الأيسر فقط ويبدأ عند 2^(P−Pt−1) لدقة P وتحويل نقطة Pt؛ وكل صف لاحق يبدأ من العينة الأعلى. والبناء يعيد الالتفاف بنمط modulo 2^16، وتحويل النقطة يقيس الخرج فقط ولا يمس سلسلة التنبؤ أبدًا. وفئة DC رقم 16 في Huffman، أي فرق 32768 بلا بتات إضافية، قانونية في وضع اللافقد فقط، لذا عليه لمحلل الجداول قبولها قبل أن يعرف أي مسار سيصرح به الإطار
يدعم PDFlibPas المتنبئين السبعة كلهم، وتحويلات النقطة، وفترات restart التي تغطي صفوفًا كاملة. ويجب أن تساوي دقة JPEG BitsPerSample وأن تكون 8 أو 16، وأن تعتمد كل مركب بنمط 1x1. وبيانات YCbCr اللافقد وJPEG اللافقد ذو 12 بت يرفضان بخطأ صريح
DCT ذو 12 بت والترميز الحسابي في Pascal الأصلي
يتشارك مفككا 12 بت والحسابي في PDFlibPas IDCT واحدًا ويتفاضلان فقط في فك الإنتروبيا. في JPEG ذي 12 بت Huffman تصل فئات DC إلى 15 وفئات AC إلى 14، وقد تحمل جداول التكميم إدخالات 16 بت، ويعاد ضبط تنبؤ DC عند كل scan وعند كل restart. تبقى المعاملات بترتيب zigzag حتى إلغاء التكميم الذي يجب أن يستخدم قيمة التكميم عند فهرس zigzag نفسه، وبعدها فقط تنتقل إلى الترتيب الطبيعي لـ IDCT ذي التمريرين القابلين للفصل. وهبوط العينات المفكوكة داخل ±1 من مفكك مرجعي مستقل ذي فاصلة عائمة
يستبدل الترميز الحسابي Huffman بمرمِّز QM من الملحق D في T.81: جدول حالات احتمال من 114 إدخالًا، ومساحة إحصاءات DC من 64 خانة، ومساحة AC من 256 خانة، توجيهها معطيات التكييف من قطعة DAC (افتراضياتها L = 0 وU = 1 وKx = 5). وتعاد حالة الإحصاءات وسياق DC والتنبؤ عند كل فترة restart وعند بداية كل scan. ولهذا يحفظ PDFlibPas قطع DAC حين يوحد JPEGTables المشتركة للصور الحسابية؛ أسقطها وفُك كل شريط بنموذج إحصاءات خاطئ
أضاف v3.723.0 مركبات أخذت العينات بنمط فرعي (subsampled) والتحويل YCbCr إلى RGB بمعادلات JFIF إلى تلك المسارات المفكوكة، وأضاف v3.724.0 الـ scans التدريجية لبيانات 12 بت Huffman (SOF2) والحسابية (SOF10) معًا. والكروما الفرعية العينة تضاعف إلى الدقة الكاملة بدل تلوينها، وهو ما ينتجه مفكك مرجعي بتعطيل تنعيم الكروما. أما JPEG الهرمي وJPEG اللافقد المرمَّز حسابيًا (SOF11) فيرفضان بخطأ صريح
اختبار fixtures JPEG ثنائية في Delphi
قد تفشل fixtures JPEG المبنية يدويًا في اختبارات وحدات Delphi بطريقة تبدو تمامًا كخطأ في المفكك. ثابت سلسلة مبني من محارف #$xx يتجاوز 255 محرفًا ينمَّط UnicodeString، وإسناده إلى AnsiString يحوله عبر صفحة الترميز النشطة. البايتات من $80 إلى $9F، وهي اعتيادية في بيانات الإنتروبيا، تعاد كتابتها في الطريق. وبناء ترويسات العلامات عند وقت التشغيل فيه المصيدة نفسها: في Delphi يعطي AnsiChar + AnsiChar UnicodeString، لذا ألحق بايتات مفردة إلى AnsiString أو اكتبها بـ SetLength والإسناد المفهرس. وللـ fixtures، خزّن نص hex وفكه عند وقت التشغيل:
uses
SysUtils, PDFlibrary;
function HexToBytes(const Hex: AnsiString): AnsiString;
var
I: Integer;
begin
SetLength(Result, Length(Hex) div 2);
for I := 1 to Length(Result) do
Result[I] := AnsiChar(StrToInt('$' + string(Copy(Hex, I * 2 - 1, 2))));
end;
// يبقى fixture ASCII صافيًا، فلا تستطيع صفحة ترميز لمسه
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);
عند توليد صور الاختبار، انتبه أن jpegtran -arithmetic يكتب تسلسلي SOF9؛ وSOF10 يحتاج -progressive أيضًا. ومعرف صورة غير صفري يثبت فقط أن الاستيراد قُبل. صيّر الصفحة وقارن البكسلات، لأن خطأ تمرير ينتج PDF حسن التشكيل لا تستطيع أي viewer رسمه
إدخال أرشيفات الماسحات القديمة إلى PDF
تميل صور JPEG تلك إلى الظهور دفعة واحدة أثناء ترحيل أرشيف. يقرأ PDFlibPas صور TIFF ذات JPEG ذي 12 بت واللافقد والحسابي والقديم عبر استدعاء AddImageFromFile نفسه كغيرها، ويحفظ الدقة الكاملة في الـ PDF، ويرفض المسارات النادرة خارج تلك المجموعة بخطأ صريح. أما التراخيص وقائمة المزايا الكاملة وتنزيلات التجربة، فراجع صفحة منتج PDFlibPas مكتبة PDF لـ Delphi