مقاله فنی

تبدیل XPS و OpenXPS به PDF در Delphi: مختصات و براش‌ها

HotPDF پکیج‌های XPS و OpenXPS را داخل Delphi و C++Builder بدون درایور چاپ به PDF تبدیل می‌کند؛ هر مختصات صفحه ثابت 96-DPI را از یک ماتریس صفحه با مقیاس 0.75 و برگرداندن Y عبور می‌دهد، هر VisualBrush را به‌شکل یک Form XObject مشترک منتشر می‌کند و مدهای کاشی ImageBrush را به‌جای رسم‌های تکراری تصویر به الگوهای کاشی‌کاری نیتیو PDF تبدیل می‌کند

سناریویی که اغلب تیم‌های ویندوزی را به این کار می‌کشد کسل‌کننده و ناگزیر است. چیزی از قبل به 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 مدخل‌های آرشیو را به 10,000 و بایت‌های آرشیو بازشده را به 1 GiB و نسبت فشرده‌سازی را به 200 و منابع را به 4,096 و صفحه‌ها را به 10,000 سقف می‌زند و یک 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 استفاده می‌شود شریک شود — فقط توکن قاعده پرکننده پیشروی F0 یا F1 توسط آداپتور XPS هندل می‌شود. اگر همین استدلال را برای ایمپورت برداری EMF و WMF دنبال کرده باشید، شکل استدلال آشناست: قالب‌های ایمپورت با ماتریس تبدیل می‌شوند، هرگز با حساب و کتاب روی مختصات‌های برگ

HotPDF ماتریس صفحه ثابت XPS را با ترنسفورم انباشته مسیر ترکیب می‌کند تا دستگاه مختصات XPS در 96-DPI با مبدأ بالا-چپ به فضای کاربر PDF در 72-DPI با مبدأ پایین-چپ برسد و به‌شکل یک عملگر cm منفرد به ازای هر visual منتشر شود
تصویر یک ماتریس می‌ماند و با هر ترنسفورم تودرتو ترکیب می‌شود، پس داده مسیر می‌تواند با اعداد دست‌نخورده XPS نوشته شود
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 رو به پایین رشد می‌کند، Y در PDF رو به بالا
  Result.E :=  0;
  Result.F := PageHeight;  // ارتفاع صفحه PDF، به پوینت
end;

// یک CTM ترکیب‌شده به ازای هر visual، منتشرشده پیش از هر عملگر مسیر
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

چطور یک VisualBrush را بدون رسم دوباره استفاده مجدد کنید؟

یک VisualBrush یک درخت visual دلخواه — فرزندان Canvas و Path و Glyphs — را داخل یک ناحیه رنگ می‌کند، شاید به‌صورت تکرارشده روی آن. HotPDF آن visual را یک بار در یک Form XObject در PDF کامپایل می‌کند و بعد جای‌گذاری‌اش می‌کند؛ همان استراتژی منبعی که در ایمپورت SVG از طریق Form XObjectها توصیف شده. دو جزئیات تعیین می‌کند کار کند یا نه. اول، visual باید به‌عنوان فرزندان مستقیم XML پیمایش شود: یک اسکن تخت برای عناصر کاشی‌شدنی، visualهای تودرتو را تا سطح بالای صفحه می‌کشد و هم scoping منبع و هم ترتیب رنگ‌آمیزی را نابود می‌کند. دوم، محتوا با ماتریس صفحه XPS-به-PDF از قبل اعمال‌شده ضبط می‌شود، پس انتشار Form نیازمند ضرب در معکوس آن ماتریس است، وگرنه هر جای‌گذاری مقیاس 0.75 و برگرداندن Y را دوباره اعمال می‌کند. Form باید مالک منابع خودش هم باشد: HotPDF فقط فونت‌ها و XObjectها و الگوها و ExtGStateها و فضاهای رنگی‌ای را کپی می‌کند که استریم محتوای ضبط‌شده واقعاً به آن‌ها ارجاع می‌دهد؛ کلون کردن کل دیکشنری منابع صفحه، همان Form در حال ثبت را به گراف منبع خودش می‌کشاند و یک چرخه می‌سازد. فونت‌ها روی صفحات معمولی در یک دیکشنری مستقیم می‌مانند و فقط وقتی محتوای ضبط‌شده واقعاً حاوی Tf است به یک دیکشنری غیرمستقیم مشترک ارتقا می‌یابند، پس سندی که visual قابل استفاده مجدد ندارد هزینه این ماشین‌آلات را نمی‌دهد. یک مرز مشخصه را هم قبل از فایل کردن باگ بدانید: بند 13.4 در ECMA-388 الزام دارد هر ViewboxUnits و ViewportUnits روی یک VisualBrush برابر Absolute باشد، پس واحدهای نسبی یک قابلیت غایب نیستند — ورودی غیرمنطبق‌اند و HotPDF از اختراع معناشناسی مختصات برایشان امتناع می‌کند

HotPDF درخت visual یک VisualBrush در XPS را یک بار در یک Form XObject در PDF کامپایل می‌کند، آن را از طریق معکوس ماتریس صفحه ثابت منتشر می‌کند تا جای‌گذاری‌ها مقیاس را دوباره اعمال نکنند و فقط منابعی را کپی می‌کند که محتوای ضبط‌شده واقعاً به آن‌ها ارجاع می‌دهد
محتوا با ماتریس صفحه از قبل اعمال‌شده ضبط می‌شود، پس Form از طریق معکوسش منتشر می‌شود و فقط منابعی را حمل می‌کند که استریم محتوای خودش به آن‌ها ارجاع می‌دهد

کاشی‌کاری ImageBrush: چهار مد، چهار اندازه سلول

مدهای کاشی XPS روی الگوهای کاشی‌کاری PDF از بند 8.7.3 در ISO 32000-1 نگاشت می‌شوند نه آن‌که به جای‌گذاری‌های تکراری تصویر روی ناحیه پوشش‌داده‌شده باز شوند؛ کاری که اندازه خروجی و زمان تبدیل را مستقل از میزان پوشش براش روی صفحه نگه می‌دارد. نگاشت وقتی ببینیدش مکانیکی است: بازتاب با گذاشتن جای‌گذاری‌های آینه‌ای داخل یک سلول الگو و بزرگ کردن سلول برای هم‌خوانی بیان می‌شود

  • Tile — یک جای‌گذاری، سلول در viewport با اندازه 1×1 می‌ماند
  • FlipX — دو جای‌گذاری، سلول به 2×1 پهن می‌شود
  • FlipY — دو جای‌گذاری، سلول به 1×2 بلند می‌شود
  • FlipXY — چهار جای‌گذاری، سلول به 2×2 گسترش می‌یابد

هر جای‌گذاری مستطیل کلیپ خودش را حمل می‌کند، چون نگاشت Viewbox‌ای که از زیرسلولش بیرون بزند به بازتاب مجاور خون‌ریزی می‌کند. /Matrix الگو همان بخشی است که آدم‌ها را می‌گیرد. یک الگوی کاشی‌کاری به فضای کاربر پیش‌فرض استریم محتوای والدش لنگر می‌شود، نه به حالت گرافیکی لحظه انتخاب الگو، پس ماتریس باید هر سه لایه را صریح ترکیب کند — تصویر صفحه ثابت، ترنسفورم Path و Transform محلی براش — به‌جای تکیه بر یک CTM محیطی. HotPDF قبل از تخصیص هم اعتبارسنجی می‌کند: RegisterImageTilingPattern یک الگو را به 1,024 جای‌گذاری محدود می‌کند و کلیپ‌های منحط و ماتریس‌های غیرمعکوس‌پذیر و ایندکس‌های تصویر نامعتبر را رد می‌کند. اگر مدل عمومی سمت 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 تعریف می‌کند، پس براش یک بیضی است. سایه‌زنی نوع 3 در PDF، در بند 8.7.4.5.4 در ISO 32000-1، بین دو دایره میان‌یابی می‌کند و راهی برای بیان مستقیم بیضی ندارد. میانگین گرفتن دو شعاع در یک عدد میان‌بُر وسوسه‌کننده‌ای است و روی هر براشی که تقریباً گرد نیست آشکارا غلط از کار درمی‌آید. 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 باید جداگانه نسبت به عرض و ارتفاع مسیر حل شوند، چون مقیاس کردن هر دو با یک طول لبه، نسبت ابعاد بیضی را روی هر مسیر غیرمربعی بی‌سروصدا عوض می‌کند

HotPDF یک RadialGradientBrush بیضوی XPS را با مقیاس کردن Y و ثبت یک سایه‌زنی دایره‌ای در فضای مقیاس‌شده و انتشار مقیاس وارونه فقط بعد از آن‌که الگو ماتریس تبدیل جاری را ثبت کرد، روی سایه‌زنی نوع 3 در PDF نگاشت می‌کند
بیضی به‌جای سایه‌زنی توسط دستگاه مختصات جذب می‌شود و مقیاس موقت باید انتخاب الگو را دقیقاً در همان ترتیب قاب بگیرد

جایی که تبدیل درباره محدودیت‌هایش صادق است

بعضی سازه‌های 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 Delphi PDF component مجموعه کامل قابلیت‌ها و نسخه‌های پشتیبانی‌شده Delphi و C++Builder را فهرست می‌کند