يحوّل HotPDF حزم XPS و OpenXPS إلى PDF داخل Delphi و C++Builder بلا برنامج تشغيل طباعة، فيُسقط كل إحداثي صفحة ثابتة بدقة 96 DPI عبر مصفوفة صفحة واحدة بمعامل قياس 0.75 وقلب لمحور Y، وينشر كل VisualBrush ككائن Form XObject مشترك، ويحوّل أنماط تبليط ImageBrush إلى نماذج تبليط PDF أصلية بدل رسم الصورة المتكرر
المشهد الذي يجرّ معظم متاجر Windows إلى هذا ممل ولا مفر منه. ثمة شيء يطبع أصلًا إلى Microsoft XPS Document Writer — تقرير ERP قديم، أو نموذج موقع، أو دفعة كشوف — وسياسة الأرشفة تقضي بـ PDF. XPS صيغة التقاط جيدة تمامًا لكنها سيئة لتسليمها لنظام سجلات بعد عقد من الآن. لذا على ملف التخزين المؤقت أن يصبح PDF صفحة بصفحة، ولحظة تبدأ كتابة ذلك المحوّل تكتشف أن الجزء المثير ليس XML. إنه أن XPS و PDF يختلفان حول موقع الأصل، وقيمة الوحدة، وما يجوز للفرشاة أن تكونه
من الحزمة إلى PDF في مرور واحد
نقطة الدخول هي سجل معالجات المستندات، لا صنف XPS خاص. تُثبِّت THPDFDocumentHandlerRegistry.RegisterStandardHandlers معالجات XPS و EPUB و CBZ؛ والتعرّف قائم على المحتوى، فالحزمة التي تحمل [Content_Types].xml مع جزء .fpage واحد على الأقل تحرز 95 حتى لو كذب امتداد الملف، بينما امتداد .xps أو .oxps وحده يحرز 10. ذلك الترتيب مهم حين تقبل المرفوعات، لأن مهاجمًا يعيد تسمية EPUB إلى .xps لا ينبغي أن يوجّه خط الأنابيب
var
Handled: IHPDFHandledDocument;
Info: THPDFDocumentHandlerInfo;
Options: THPDFDocumentHandlerOptions;
Registry: THPDFDocumentHandlerRegistry;
Output: TFileStream;
begin
Registry := THPDFDocumentHandlerRegistry.Create;
Output := TFileStream.Create('spool.pdf', fmCreate);
try
Registry.RegisterStandardHandlers;
Options := THPDFDocumentHandlerOptions.Default;
if not Registry.OpenFromFile('spool.xps', '', Options, Handled, Info) then
raise Exception.Create('no registered handler recognised the package');
if Handled.Format = hdfXPS then
Handled.WritePDF(Output, Options, Info);
// Info.UnsupportedFeatureCount هو التقييم الأمين لهذا التحويل
finally
Output.Free;
Registry.Free;
end;
end;
كل شيء في التحويل محدود قبل محاولته. تضع THPDFDocumentHandlerOptions.Default سقفًا لمداخل الأرشيف عند 10000، ولبايتات الأرشيف بعد الفك عند 1 GiB، ولنسبة الضغط عند 200، وللموارد عند 4096، وللصفحات عند 10000، وتحمل CancellationToken اختياريًا كي يمكن إيقاف مهمة خادمية في منتصف الحزمة. اقرأ Info.UnsupportedFeatureCount بعد ذلك وعامل قيمة غير صفرية كنتيجة حقيقية: يتعمّد HotPDF إحصاء ما عجز عن إسقاطه بدل أن يرسم تقريبًا ويسكت عنه
لماذا تحتاج صفحة XPS إلى مصفوفة بدل إعادة كتابة الإحداثيات؟
لأن إعادة كتابة الإحداثيات تضيع مكدس التحويلات. تُحدَّد FixedPage في XPS بوحدات 96 DPI والأصل أعلى اليسار ومحور Y ينمو نزولًا؛ أما فضاء مستخدم PDF فبدقة 72 DPI والأصل أسفل اليسار ومحور Y ينمو صعودًا. الإصلاح الساذج هو ضرب كل رقم في 0.75 وطرح كل Y من ارتفاع الصفحة أثناء إصداره. ينجح ذلك لمسار مسطح واحد وينهار لحظة يدخل RenderTransform أو Canvas متداخلة أو مصفوفة محلية للفرشاة، لأن تلك التحويلات معرَّفة في فضاء XPS وإعادة كتابتك لكل إحداثي على حدة قد غادرته أصلًا. لذلك يبقي HotPDF الإسقاط مصفوفة ويؤلّفها. تُرجع HPDFXPSPageMatrix الثوابت مرة لكل صفحة، وتدمج HPDFMultiplyXPSMatrix المصفوفة مع تحويل المسار المتراكم، وتُصدَر النتيجة كعامل cm واحد قبل الهندسة. ثم تُكتب بيانات المسار بأرقام XPS غير المعدّلة، وهو أيضًا سبب أن صيغة الهندسة المختصرة تتشارك المحلل المحدود نفسه المستخدم لبيانات مسارات SVG — ولا يعالج محوّل XPS إلا توكن قاعدة الملء F0 أو F1 في المقدمة. وإن تابعت المنطق نفسه في استيراد المتجهات من EMF و WMF فشكل الحجة مألوف: صيغ الاستيراد تُحوَّل بالمصفوفات، لا بحساب تجريه على إحداثيات الأوراق
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // من وحدة XPS بدقة 96 DPI إلى نقطة PDF بدقة 72 DPI
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // محور Y في XPS ينزل، وفي PDF يصعد
Result.E := 0;
Result.F := PageHeight; // ارتفاع صفحة PDF بالنقاط
end;
// CTM مؤلَّف واحد لكل عنصر مرئي، يُصدر قبل أي عامل مسار
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
كيف تعيد استخدام VisualBrush دون رسمها مرتين؟
تطلو VisualBrush شجرة مرئية اعتباطية — أبناء من Canvas وPath وGlyphs — داخل منطقة، وقد تكررها عبرها. يجمع HotPDF ذلك العنصر المرئي مرة واحدة في كائن Form XObject خاص بـ PDF ثم يضعه، وهي استراتيجية الموارد نفسها الموضحة في استيراد SVG عبر كائنات Form XObject. تفصيلان يقرّران نجاح الأمر. أولًا، يجب أن تُجتاز الشجرة كأبناء XML مباشرين: فالمسح المسطح بحثًا عن عناصر قابلة للتبليط يسحب العناصر المرئية المتداخلة إلى المستوى الأعلى للصفحة ويدمّر نطاق الموارد وترتيب الطلاء معًا. ثانيًا، المحتوى يُلتقط ومصفوفة صفحة XPS إلى PDF مطبّقة أصلًا، لذا نشر Form يتطلب الضرب بمعكوس تلك المصفوفة، وإلا أعاد كل موضع تطبيق قياس 0.75 وقلب Y. وعلى Form أن يملك موارده: لا ينسخ HotPDF إلا الخطوط وكائنات XObject والنماذج وحالات ExtGState والفضاءات اللونية التي يشير إليها تدفق المحتوى الملتقط فعلًا؛ فاستنساخ قاموس موارد الصفحة كاملًا سيسحب Form الجاري تسجيله إلى رسم موارده ويبني حلقة. الخطوط تبقى في قاموس مباشر على الصفحات العادية ولا تُرقَّى إلى قاموس غير مباشر مشترك إلا حين يحوي المحتوى الملتقط Tf فعلًا، فلا تدفع وثيقة بلا عناصر مرئية قابلة لإعادة الاستخدام ثمن تلك الآلية. ولاحظ حدًا مواصفيًا يجدر معرفته قبل أن ترفع بلاغًا: تتطلب ECMA-388 القسم 13.4 أن تكون كل من ViewboxUnits وViewportUnits على VisualBrush بقيمة Absolute، فالوحدات النسبية ليست ميزة مفقودة — إنها إدخال غير مطابق، ويرفض HotPDF أن يخترع لها دلالات إحداثية
تبليط ImageBrush: أربعة أنماط وأربعة أحجام خلايا
تُسقَط أنماط تبليط XPS على نماذج التبليط في PDF من ISO 32000-1 القسم 8.7.3 بدل أن تُفكَّك إلى مواضع صور متكررة عبر المنطقة المغطاة، وهو ما يبقي حجم الخرج وزمن التحويل مستقلين عن القدر الذي تغطيه الفرشاة من الصفحة. الإسقاط ميكانيكي حين تراه: الانعكاس يُعبَّر عنه بوضع مواضع معكوسة داخل خلية نموذج واحدة وتكبير الخلية لتطابق
Tile— موضع واحد، والخلية تبقى 1×1 مقاس منفذ العرضFlipX— موضعان، والخلية تُوسَّع إلى 2×1FlipY— موضعان، والخلية يُرفع ارتفاعها إلى 1×2FlipXY— أربعة مواضع، والخلية تُمدَّد إلى 2×2
كل موضع يحمل مستطيل قص خاصًا به، لأن إسقاط Viewbox يتجاوز خليته الفرعية سيسيل إلى الانعكاس المجاور. أما /Matrix الخاص بالنموذج فهو الجزء الذي يعلق بالناس. نموذج التبليط يُرسى على فضاء المستخدم الافتراضي لتدفق المحتوى الأب، لا على حالة الرسوم الراهنة لحظة اختيار النموذج، لذا على المصفوفة أن تؤلّف الطبقات الثلاث صراحة — إسقاط الصفحة الثابتة، وتحويل المسار، وTransform المحلية للفرشاة — بدل الاتكال على CTM محيطة. ويتحقق HotPDF أيضًا قبل أن يخصّص: تحدّ RegisterImageTilingPattern النموذج بـ 1024 موضع وترفض القص المتحلل والمصفوفات غير القابلة للعكس وفهارس الصور غير الصالحة. وإن أردت النموذج العام على جانب PDF خلف هذا، فمقال نماذج التبليط وفضاء ألوان Pattern يغطي العوامل الأساسية
// إسقاط الصفحة الثابتة مدمج في مصفوفة النموذج، ثم مصفوفة الفرشاة المحلية
PatternMatrix := HPDFMatFromOps( 0.75 * PathMatrix.A, -0.75 * PathMatrix.B,
0.75 * PathMatrix.C, -0.75 * PathMatrix.D,
0.75 * PathMatrix.E,
PageHeight - 0.75 * PathMatrix.F);
if Brush.HasTransform then
PatternMatrix := HPDFMatMul(PatternMatrix, BrushMatrix);
PatternName := Document.RegisterImageTilingPattern(Resource.ImageIndex,
Brush.Viewport.Left, Brush.Viewport.Top,
Brush.Viewport.Left + CellWidth, Brush.Viewport.Top + CellHeight,
CellWidth, CellHeight, Placements, pttNoDistortion, PatternMatrix);
ماذا يحدث حين لا يكون التدرج الشعاعي دائرة؟
تعرّف XPS RadialGradientBrush بـ GradientOrigin وCenter وRadiusX وRadiusY، فتكون الفرشاة إهليلجًا. أما تظليل PDF من النوع 3، في ISO 32000-1 القسم 8.7.4.5.4، فيمزج بين دائرتين ولا يملك وسيلة للتعبير عن إهليلج مباشرة. حساب متوسط نصفَي القطر في رقم واحد هو الاختصار المغري وهو خطأ مرئي في أي فرشاة غير قريبة من الاستدارة. بدل ذلك ينقل HotPDF المشكلة إلى نظام الإحداثيات: يقيّس Y بنسبة RadiusY / RadiusX، ويسجّل تظليلًا دائريًا أمينًا في ذلك الفضاء المقيَّس، ويختار النموذج، ثم يصدر القياس المعكوس فورًا كي تبقى هندسة المسار التالية في فضاء مستخدم XPS الأصلي
ScaleY := Brush.RadiusY / Brush.RadiusX;
PatternName := Document.RegisterMultiStopRadialGradient(
Brush.StartX, Brush.StartY / ScaleY, 0,
Brush.EndX, Brush.EndY / ScaleY, Brush.RadiusX,
StopPositions, StopColours, 3);
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(ScaleY) + ' 0 0 cm'#10);
Page.SetFillPattern(PatternName); // النمط يلتقط CTM هنا تمامًا
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
الترتيب في ذلك المقتطف هو الحيلة كلها، وهو ليس تنسيقيًا. نموذج تظليل PDF يلتقط مصفوفة التحويل الراهنة لحظة اختياره كلون حالي، لذا القياس المؤقت يجب أن يُصدَر قبل SetFillPattern أو SetStrokePattern، والمعكوس يجب أن يتبع الاختيار لكنه يسبق عوامل المسار. أخطئ الترتيب في أي اتجاه يكن لديك تدرج يُصيَّر صحيحًا على أول مسار وينزاح على كل مسار يليه. وثمة قيد متصل بوضع الإحداثيات النسبية: يجب حل RadiusX وRadiusY مقابل عرض المسار وارتفاعه كل على حدة، لأن قياس كليهما بطول حافة واحدة يغيّر بصمت النسبة الباعية للإهليلج على أي مسار غير مربع
حيث يصدق التحويل عن حدوده
بعض عناصر XPS تُحوَّل تقريبًا وبعضها لا يُحوَّل إطلاقًا، والخيار في كل موضع هو الإحصاء لا الاختلاق. أجزاء TIFF و JPEG XR تُنقَّط عبر WIC بلا وعد بالحفاظ على alpha، بينما تُشطَر PNG ذات قناة alpha صالحة إلى صورة أساس مع /SMask. الحجم الذاتي للصورة يُشتق كـ pixel * 96 / DPI، بقراءة كثافة pHYs في PNG أو JFIF في JPEG أولًا ثم الرجوع إلى 96 DPI، فترسو رأسية كثافة رديئة على حجم متوقع لا على حجم اعتباطي. وموارد المصفوفات غير المحلولة، والتحويلات النسبية غير القياسية، وColorConvertedBitmap، وأنماط انتشار التدرج غير المدعومة، والهندسة المشوّهة، كلها تزيد UnsupportedFeatureCount، والإدخال المشوّه يفشل بإغلاق بدل أن يتدهور إلى رسم مختلف بصمت
ذلك هو الوضع المفيد لمحوّل أرشفة: تحويل يقترب بصمت أسوأ من تحويل يخبرك أي عناصره الأربعة عجز عن تمثيلها، لأن الثاني وحده يعطيك ما تتحقق منه قبل أن يُختم المستند في نظام سجلات. إن كنت تقيّم تحويل XPS و OpenXPS إلى جانب بقية خط أنابيب المستندات — تجميع الصفحات والخطوط والتوقيع وخرج PDF/A — فإن صفحة مكوّن HotPDF لـ PDF في Delphi تسرد مجموعة الميزات الكاملة وإصدارات Delphi و C++Builder المدعومة