JPEG ذو CMYK يخرج من PDF بألوان عكرة أو منحرفة أو سالبة يعني في الغالب أن المفكك أخطأ في واحدة من حقيقتين: هل خزنت العينات كـ YCCK وتحتاج تحويلًا، أم خزنت معكوسة باصطلاح Adobe. ومفككات HotPDF تحترم معامل /ColorTransform الخاص بـ DCTDecode للصور ذات ثلاث قنوات وأربع، وترفض القيم التي لا تعرفها المواصفة، وتجعل علامة Adobe APP14 داخل بيانات JPEG تتقدم على ذلك الإدخال في القاموس، كما يشترط الجدول 13 من ISO 32000-1. وقاعدتان متجاورتان تعودان لعائلة المصائد نفسها: يجب أن تحمل الـ streams متعددة الفلاتر مصفوفات /DecodeParms متوافقة المراحل، وتحفظ صور JPEG 2000 خريطة /Decode صريحة
الثلاثة تتشارك موضوعًا واحدًا: الـ PDF يقول معناه مسبقًا، والمصيدة هي مصير يجيب عن سؤال مختلف قليلًا
لماذا يعرض JPEG ذو CMYK معكوسًا أو باهتًا في PDF؟
يخيب JPEG ذو CMYK في PDF لسببين أحدهما، ويبديان اختلافًا على الشاشة: الصورة السالبة، حيث يصير الورق الأبيض أسود والصبغات الفاتحة داكنة، تشير إلى تخزين معكوس. والصورة الباهتة أو المنحرفة الصبغة، حيث يخضر الجلد ويشيب الأسود مع بقاء تفاصيل K حادة، تشير إلى تحويل لون طُبق حين لم يكن واجبًا أو تُرك حين كان مطلوبًا
مشكلة التحويل مصدرها طريقة ضغط JPEG للون: يضغط JPEG baseline أفضل ما يكون حين ينفصل التوهج (luminance) عن التلوين (chrominance)، فيحوّل المرمزون RGB إلى YCbCr قبل الضغط عادة، ومرمزو القنوات الأربع يحولون CMY إلى YCC مع تمرير K دون مساس، وهي الصيغة المسماة YCCK. وعلى المفكك أن يلغي بالضبط التحويل الذي طبقه المرمِّز: ألغِ تحويلًا لم يحدث، أو ترك تحويلًا حدث، فيتحرك كل بكسل. قناة الأسود تنجو في الحالين، ولهذا غالبًا ما يحفظ صور CMYK المعطوب نصًا أسود حادًا بينما ينحرف كل ما عداه
مشكلة الانعكاس تاريخية: تكتب Photoshop وأدوات Adobe الأخرى ملفات CMYK JPEG بكل عينة مخزنة كـ 255 - value، وتميّز الملف بعلامة Adobe APP14. لا شيء في معيار JPEG يقول ذلك؛ إنه اصطلاح، وعلى كل مستهلك أن يعرفه. وفي PDF ينتظر من المنتج أن يعبر عن الانعكاس في قاموس الصورة بدل تركه للتخمين
ماذا يقول /ColorTransform للمفكك؟
/ColorTransform هو التصريح على مستوى PDF عن أي تحويل يجب على المفكك إلغاؤه. يعرف §7.4.8 من ISO 32000-1، في الجدول 13، قيمتين: الصفر يعني بلا تحويل فيعيد المفكك العينات كما ضغطت، والواحد يعني أن المرمِّز طبق التحويل القياسي، فتعود صورة ثلاثية القنوات من YCbCr إلى RGB ورباعية القنوات من YCCK إلى CMYK. ويهمل المعامل في الصور ذات قناة أو قناتين. كما يضع الجدول 13 قاعدة أولوية سهلة الفقدان: حين تحمل البيانات المرمَّزة علامة Adobe بوسم تحويل خاص بها، فذاك الوسم يحكم ويهمل إدخال القاموس. ولا يعود إلى القاموس إلا JPEG بلا تلك العلامة، وحين يكون الإدخال غائبًا أيضًا فإن صورة ثلاثية القنوات تعتمد التحويل افتراضيًا ورباعية القنوات تعتمد لا شيء
الخطأ السهل هو مسار صورة يقبل قاموس المعاملات ولا يسلمه لمفكك JPEG أبدًا. يفضحه فورًا ملف RGB JPEG من بكسل واحد يحمل اللون (128, 63, 31): فذلك المفكك يعيد [128, 63, 31] مع /ColorTransform 0 و/ColorTransform 1 معًا، بينما الجواب الصحيح عن الصفر هو الثلاثي YCbCr الخام [79, 101, 163]
يطبق HotPDF المعامل على مستوى البايتات: يمشي HPDFDecodeHeadlessTerminalImage أولًا في بنية قطع JPEG حتى علامة بدء الـ scan ويبحث عن قطعة Adobe APP14؛ والبايتات التي تشبهها فحسب داخل قطعة تعليق لا تحتسب. فإن حملت البيانات العلامة، قرر وسم تحويلها وأهملت قيمة القاموس. وإن لم تحملها، بنى المفكك نسخة خاصة من JPEG بقطعة APP14 مدرجة قبل علامة بدء الـ scan مباشرة، تحمل وسم تحويل 0 أو 1، أو وسم YCCK وهو 2 لصورة رباعية القنوات بقيمة 1. وكلما كان التحويل الفعلي لصورة ثلاثية القنوات 0، عطّلت النسخة ترويسة JFIF أيضًا، لأن ترويسة JFIF بمفردها تخبر libjpeg-turbo بافتراض YCbCr. لا يلمس buffer المستدعي مطلقًا، وحين تدرج قطعة تحتسب بايتاتها الـ 16 الإضافية على ميزانية بايتات العمل قبل أن يخصص المفكك أي شيء
الإدراج فقط حين لا توجد علامة مهم بسبب تفصيلة في libjpeg: فهي تستخدم آخر قطعة APP14 تراها. أدخل قطعة بعد علامة قائمة فيتجاوز قيمة قاموس صريحة بصمت علامة متعارضة في البيانات، وهذا عكس الجدول 13 تمامًا. ويطبق HotPDF ترتيب الجدول 13 على مسار image XObject الكامل وعلى مسار الصورة اللاصقة (inline) على السواء
تستطيع استدعاء المفكك الطرفي نفسه مباشرة حين تبدو صورة واحدة خاطئة وتريد رؤية عيناتها دون تصيير الصفحة. توجد الدالة في وحدة HPDFHeadlessImageCodecs، تأخذ اسم الـ filter والبايتات المرمَّزة وقاموس معاملات اختياريًا والهندسة من قاموس الصورة، الذي يجب أن يطابق ترويسة إطار JPEG
uses
SysUtils, HPDFObjs, HPDFHeadlessImageCodecs;
// ColorTransform < 0 يعني "الإدخال غائب": اترك علامات JPEG تقرر
function DecodeDCT(const Encoded: TBytes; Width, Height, Channels,
ColorTransform: Integer): TBytes;
var
Parms: THPDFDictionaryObject;
begin
Parms := nil;
if ColorTransform >= 0 then
begin
Parms := THPDFDictionaryObject.Create(nil);
Parms.AddNumericValue('ColorTransform', ColorTransform);
end;
try
Result := HPDFDecodeHeadlessTerminalImage('DCTDecode', Encoded, nil, Parms,
Width, Height, Channels, 8, 64 * 1024 * 1024, nil);
finally
Parms.Free;
end;
end;
المفكك صارم فيما يعد قيمة قانونية: -1 صريحة، أو 2، أو غير صحيح مثل 0.5 ترفع EHPDFHeadlessImageError بدل العودة إلى افتراض، لأن قيمة لا تعرفها المواصفة لا تعطي المفكك شيئًا موثوقًا يعمل عليه. ويبقى الفحص قائمًا حتى حين تجعل علامة Adobe في البيانات الإدخال بلا معنى، لأن القيمة غير المعرفة خطأ بنيوي في المستند نفسه. وميزانية صغيرة على النسخة المحولة تفشل بالطريقة نفسها، برسالة تسمي ميزانية بايتات العمل
أما null الصريحة فحالة مختلفة، لأنها ليست قيمة أصلًا: يقول §7.3.9 من ISO 32000-1 إن إدخال قاموس قيمته null يعادل إدخالًا غائبًا، فتهبط /ColorTransform null إلى العلامة أو إلى افتراضي عدد المركبات تمامًا كأن المفتاح مفقود. ويعاملها HotPDF هكذا على كل مسار، headless وVCL، وstream وinline على السواء
افتراض آخر يستحق ذكرًا، لأنه الموضع الذي يتجاوز فيه HotPDF بحسب قصد حرف الجدول 13: JPEG ثلاثي المركبات بلا ترويسة JFIF وبلا علامة Adobe، ترويسة إطاره تسمي مركباته R وG وB، يقول في بايتاته أنه يحمل RGB. سيجعل الجدول 13 معياره افتراضيًا التحويل، لكن libjpeg والقارئات المبنية عليها تفكه بلا تحويل، ويفعل HotPDF الشيء نفسه. ومعالجة المفككين headless وVCL كلاهما على ذلك، فيبقى الأحمر والأزرق حيث وضعتهما البيانات. ولا يزال قاموس صريح بقيمة 0 أو 1 يتجاوز هذا الاستنتاج
uses
System.IOUtils;
var
Jpeg, Stored, Converted, ByMarkers: TBytes;
begin
// بايتات DCTDecode الخام لصورة CMYK بمقاس 640 x 480، محفوظة من stream الـ PDF
Jpeg := TFile.ReadAllBytes('suspect-image.jpg');
try
// مع وجود علامة Adobe APP14 في البيانات، تتوافق الاستدعاءات الثلاثة:
// العلامة تقرر وقيمة القاموس تهمل
Stored := DecodeDCT(Jpeg, 640, 480, 4, 0); // بلا علامة: العينات كما ضغطت
Converted := DecodeDCT(Jpeg, 640, 480, 4, 1); // بلا علامة: فك YCCK إلى CMYK
ByMarkers := DecodeDCT(Jpeg, 640, 480, 4, -1); // بلا علامة: افتراضي أربع قنوات، أي لا شيء
Writeln(Format('pixel 0: stored %d,%d,%d,%d converted %d,%d,%d,%d',
[Stored[0], Stored[1], Stored[2], Stored[3],
Converted[0], Converted[1], Converted[2], Converted[3]]));
except
on E: EHPDFHeadlessImageError do
Writeln('Decoder refused the image: ', E.Message);
end;
end;
بكسل مرجعي رباعي القنوات يريك ما تبحث عنه: بكسل CMYK بقيم (10, 20, 30, 40) خزن كـ YCCK يعود [245, 235, 225, 215] حين لا يلغى تحويل YCCK و[0, 116, 0, 215] حين يلغى. وبايت K، أي 215، متطابق في الحالتين. فإن بدت مستوية K صحيحة والأخرى الثلاث لا، فالتحويل غير المطابق هو المشتبه به الأول
اصطلاح CMYK المعكوس يعود إلى /Decode
يعيد مفكك HotPDF الطرفي عينات CMYK كما ينتجها libjpeg-turbo تمامًا ولا يقلبها بقوة علامة Adobe. الأرقام أعلاه تجعل الاصطلاح مرئيًا: 245 و235 و225 و215 هي ببساطة 255 ناقص 10 و20 و30 و40، الصيغة المعكوسة التي يخزنها مرمزو أسلوب Adobe. وإعادتها إلى كميات حبر هي عمل قاموس الصورة، حيث يكتب المنتج الذي يضمّن JPEG كهذا /Decode [1 0 1 0 1 0 1 0]. ويطبق المصير مصفوفة /Decode على كل مركب، فالملف المكتوب صحيحًا يطبع صحيحًا
والوجه المقابل يستحق قولًا صريحًا: سيصير PDF يضمّن JPEG ذو CMYK معكوس بأسلوب Adobe دون مصفوفة /Decode المطابقة كصورة سالبة في HotPDF، لأن الملف نفسه يصف الصورة هكذا. بعض viewers تكشف علامة APP14 وتعوض بصمت. ولا يضيف HotPDF ذلك الاستدلال، فإصلاح مثل ذلك الملف موضعُه البرنامج الذي كتبه
لماذا يكون قاموس DecodeParms واحد لفلترين خطأ؟
يجب على stream يكون /Filter خاصتها مصفوفة من عدة مراحل أن يعطي /DecodeParms مصفوفة بالطول نفسه، قاموس معاملات واحد أو null لكل مرحلة. ينص على تلك القاعدة §7.3.8.2 من ISO 32000-1 في الجدول 5، ويفرضها HotPDF: الـ stream متعدد الفلاتر الذي يعطي قاموسًا مشتركًا واحدًا يسجل مستندًا غير صالح بدل فكه
البديل المتساهل أسوأ مما يبدو: مفكك يفحص الطول فقط حين تكون /DecodeParms مصفوفة أصلًا سيعطى قاموسًا واحدًا وفلترين فيعيد استخدام ذلك القاموس بصمت في كل مرحلة. ذلك غير مؤذن والقاموس فارغ، ومدمر حين ليس فارغًا: /Predictor 12 معني بمرحلة Flate الداخلية سيطبق على الخارجية أيضًا، ويصير الخرج فوضى تبدو معقولة بدل خطأ نظيف. ولأن قصد مثل ذلك الملف لا يمكن استرجاعه، يرفضه HotPDF حتى في الحالة الفارغة، مثل stream محتوى صفحة بـ /Filter [/FlateDecode /FlateDecode] و/DecodeParms << >>
الأشكال المقبولة والمرفوضة سهلة العد:
- مقبول: حذف
/DecodeParmsأو وضعnullمهما كان عدد الفلاتر - مقبول: مصفوفة طولها يساوي عدد الفلاتر، كل إدخال فيها قاموس أو
null - مقبول: قاموس واحد حين تسمي
/Filterفلترًا واحدًا بالضبط - مرفوض: قاموس واحد يتشاركه فلتران أو أكثر، مع التشخيص "Stream filter chain requires array DecodeParms"
- مرفوض: مصفوفة طولها يخالف عدد الفلاتر، مع "DecodeParms array must match the filter chain"
يعمل الفحص نفسه على streams المحتوى العادية وعلى streams الصور التي آخر فلاترها codec طرفي مثل DCTDecode، فيفشل الـ chain المشوه في الموضع نفسه مهما حمل الـ stream. وتفاعل ذلك مع حدود فك الضغط مغطى في مقالة ميزانيات سلاسل الفلاتر وقنابل فك PDF. ومن كود التطبيق يظهر الخطأ عند تصيير الصفحة
uses
SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;
var
Document: THPDFHeadlessDocument;
Options: THPDFHeadlessRenderOptions;
Info: THPDFHeadlessRenderResult;
begin
Document := THPDFHeadlessDocument.Create;
try
try
Document.LoadFromFile('catalog-cmyk.pdf');
Options := THPDFHeadlessRenderOptions.Default;
Info := HPDFRenderHeadlessPagePNGFile(Document.GetRenderAccess(0),
'page1.png', Options);
Writeln('images=', Info.Images, ' decoded bytes=', Info.DecodedBytes);
except
on E: Exception do
Writeln('Rejected: ', E.ClassName, ': ', E.Message);
end;
finally
Document.Free;
end;
end;
صور JPEG 2000 تحفظ /Decode الصريحة
تتجاهل صورة JPEG 2000 /Decode فقط حين لا يكون في قاموسها /ColorSpace وليست قناع صورة. يجعل §8.9.5 من ISO 32000-1 ذلك مشروطًا، ويتتبعه HotPDF. وإسقاط /Decode عن كل صورة JPX غير stencil هو الطريق المختصر السهل، وهو يفني انعكاسًا صريحًا ببساطة: عينة JPX DeviceGray بقيمة 64 تحت /Decode [1 0] يجب أن تصير 191 لا 64
إصابة ذلك الشرط الواحد لا تكفي وحدها: فروع فضاءات الألوان مثل Lab وICCBased التي تضبط مدى المركبات الافتراضي كلما كانت الصورة JPX ستكتب فوق مصفوفة طبقت قانونيًا مرة ثانية، لذا يسجل المصير هل طبقت مصفوفة /Decode صالحة فعلًا، ويملأ المدى الافتراضي فقط حين لم تطبق أي مصفوفة. فتنجو مدى Lab وICCBased الصريحة، ولا يزال تصريح لون الـ codestream يحكم حين يصمت القاموس. وجهة المفكك في الصيغة موصوفة في مقالة JPEG 2000 وJPXDecode
أي مسارات الفك تتبع هذه القواعد؟
الخطان يتبعانها: على جانب headless يعني ذلك تحميل THPDFHeadlessDocument، ومفكك الـ streams المشترك، ومصير الصفحات المبني على Cairo خلف HPDFRenderHeadlessPagePNGFile. وعلى جانب VCL يعني ذلك استخراج الصور المحملة إلى TBitmap وتصيير صفحات VCL، اللذان يطبقان ترتيب الجدول 13 نفسه على فك الصورة الكاملة والمنطقة المصغرة على السواء، فلا تستطيع ترويسة JFIF فرض YCbCr فوق تحويل Adobe بقيمة 0، والقيمة غير القانونية تجعل الصورة غير قابلة للفك هناك أيضًا. وتتبع صور DCT اللاصقة قواعد العلامة وnull والافتراض والرفض نفسها الخاصة بـ image XObjects، بالأسماء المختصرة والكاملة للمفاتيح على السواء، بما فيها قاموس /DP << … >> ومصفوفات فلاتر مختصرة مثل /F [/AHx /DCT]، وتتخطى صورة لاصقة بمعاملات غير قانونية بينما يصير باقي الصفحة. دليل استخراج الصور من PDF محمل يغطي بقية تعامل ذلك الـ API مع الفلاتر
بضع ملاحظات عملية قبل أن تعتمد على هذا السلوك:
- فك JPEG يحتاج مكتبة TurboJPEG متوافقة عند وقت التشغيل؛ و
HOTPDF_TURBOJPEG_LIBRARYتتجاوزturbojpeg.dllأوlibturbojpeg.so.0الافتراضية - الفحوص البنيوية الصارمة ترفض ملفات تفتحها قارئات متساهلة، أبرزها قاموس
/DecodeParmsالمشترك الفارغ، وإصلاح تلك الملفات موضعُه المنتج
في مسار طبقة مُدار الألوان، الخلاصة العملية قصيرة: اكتب ملفات CMYK JPEG بـ /ColorTransform صريحة توافق أي علامة Adobe في البيانات، مع مصفوفة /Decode المطابقة لتخزينها، وأعط كل stream متعدد الفلاتر مصفوفة /DecodeParms متوافقة المراحل، وتوقع من HotPDF أن يمسك الملفات بما تقوله فعلًا
المحمل headless ومفككات الـ streams ومصير صفحات Cairo المعروضة هنا تأتي مع مكوّن HotPDF لـ PDF في Delphi لـ Delphi وC++Builder وLazarus، مع محرك مستندات VCL الكلاسيكي