Teknik Makale

Delphi'de XPS ve OpenXPS Dönüşümü: Koordinatlar, Fırçalar

HotPDF, XPS ve OpenXPS paketlerini Delphi ve C++Builder içinde yazdırma sürücüsü olmadan PDF biçimine dönüştürür: her 96 DPI sabit sayfa koordinatını tek bir 0.75 ölçekli, Y eksenini çeviren sayfa matrisinden geçirir, her VisualBrush öğesini paylaşılan bir Form XObject olarak yayınlar ve ImageBrush döşeme kiplerini yinelenen görüntü çizimleri yerine yerli PDF döşeme desenlerine çevirir

Çoğu Windows şirketini bu işe sürükleyen senaryo sıkıcı ve kaçınılmazdır. Bir şeyler zaten Microsoft XPS Document Writer yazıcısına yazdırılıyordur — eski bir ERP raporu, imzalı bir form, bir ekstre toplusu — ve arşiv politikası PDF diyordur. XPS mükemmel bir yakalama biçimidir ama on yıl sonra bir kayıt sistemine teslim edilecek korkunç bir biçimdir. Bu yüzden biriktirici dosyasının sayfa sayfa PDF olması gerekir ve o dönüştürücüyü yazmaya başladığınız anda ilginç kısmın XML olmadığını keşfedersiniz. Asıl mesele XPS ile PDF biçiminin orijin konumu, birimin değeri ve bir fırçanın ne olabileceği konularında uyuşmamasıdır

Paketten PDF biçimine tek geçişte

Giriş noktası özel bir XPS sınıfı değil, belge işleyici kayıt defteridir. THPDFDocumentHandlerRegistry.RegisterStandardHandlers, XPS, EPUB ve CBZ işleyicilerini kurar; tanıma içerik tabanlıdır, bu yüzden [Content_Types].xml artı en az bir .fpage parçası taşıyan bir paket, dosya uzantısı yalan söylediğinde bile 95 puan alırken yalın bir .xps ya da .oxps uzantısı ancak 10 puan alır. Karşıya yükleme kabul ettiğinizde bu sıralama önemlidir; çünkü bir EPUB dosyasını .xps olarak yeniden adlandıran bir saldırgan işlem hattını yönlendirememelidir

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 bu dönüşümün dürüst puanıdır
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Dönüşümle ilgili her şey denenmeden önce bütçelenir. THPDFDocumentHandlerOptions.Default, arşiv girdilerini 10.000, açılmış arşiv baytlarını 1 GiB, sıkıştırma oranını 200, kaynakları 4.096 ve sayfaları 10.000 ile sınırlar ve isteğe bağlı bir CancellationToken taşır; böylece sunucu tarafı bir iş paket ortasında durdurulabilir. Sonradan Info.UnsupportedFeatureCount değerini okuyun ve sıfır olmayan bir değeri gerçek bir bulgu sayın: HotPDF, eşleyemediği şeyi yaklaşık bir çizim yapıp susmak yerine bilerek sayar

Bir XPS sayfası neden yeniden yazılmış koordinatlar yerine matrise ihtiyaç duyar?

Çünkü koordinatları yeniden yazmak dönüşüm yığınını kaybeder. Bir XPS FixedPage, orijin sol üstte ve Y aşağı büyüyen 96 DPI birimlerinde belirtilir; PDF kullanıcı uzayı ise orijin sol altta ve Y yukarı büyüyen 72 DPI birimidir. Saf çözüm, her sayıyı 0.75 ile çarpmak ve yayarken her Y değerini sayfa yüksekliğinden çıkarmaktır. Bu tek düz bir yol için işler ama bir RenderTransform, iç içe bir Canvas ya da fırça yereli matris girer girmez dağılır; çünkü o dönüşümler XPS uzayında tanımlıdır ve koordinat başına yeniden yazımınız o uzayı çoktan terk etmiştir. HotPDF bu yüzden izdüşümü bir matris olarak tutar ve bileştirir. HPDFXPSPageMatrix sabit katsayıları sayfa başına bir kez döndürür, HPDFMultiplyXPSMatrix onu birikmiş yol dönüşümüyle art arda bağlar ve sonuç geometriden önce tek bir cm işleci olarak yayılır. Yol verisi sonra değiştirilmemiş XPS sayılarıyla yazılır; kısaltılmış geometri söz diziminin SVG yol verisi için kullanılan sınırlı ayrıştırıcıyı paylaşabilmesinin nedeni de budur — yalnızca baştaki F0 ya da F1 dolgu kuralı simgesi XPS bağdaştırıcısı tarafından işlenir. EMF ve WMF vektör içe aktarımı için aynı akıl yürütmeyi izlediyseniz argümanın biçimi tanıdıktır: içe aktarma biçimleri matrisle dönüştürülür, asla yaprak koordinatlar üzerinde aritmetikle değil

HotPDF, sabit XPS sayfa matrisini birikmiş yol dönüşümüyle bileştirir; böylece 96 DPI sol üst orijinli XPS koordinat sistemi, orijini sol altta olan 72 DPI PDF kullanıcı uzayına ulaşır ve görsel başına tek bir cm işleci olarak yayılır
İzdüşüm matris olarak kalır ve her iç içe dönüşümle bileştirilir; böylece yol verisi değiştirilmemiş XPS sayılarıyla yazılabilir
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // 96 DPI XPS biriminden 72 DPI PDF punto birimine
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // XPS tarafında Y aşağı büyür, PDF tarafında Y yukarı büyür
  Result.E :=  0;
  Result.F := PageHeight;  // PDF sayfa yüksekliği, punto cinsinden
end;

// Görsel başına tek bileşik CTM, herhangi bir yol işlecinden önce yayılır
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Bir VisualBrush öğesini iki kez çizmeden nasıl yeniden kullanırsınız?

Bir VisualBrush, keyfi bir görsel ağacı — Canvas, Path ve Glyphs çocukları — bir bölgeye boyar, olasılıkla bölge boyunca yinelenir. HotPDF o görseli bir kez PDF Form XObject olarak derler ve sonra yerleştirir; bu, Form XObject ile SVG içe aktarma konusunda anlatılan kaynak stratejisinin aynısıdır. İki ayrıntı işe yarayıp yaramayacağına karar verir. Birincisi, görsel doğrudan XML çocukları olarak gezilmelidir: döşenebilir öğeler için düz bir tarama, iç içe görselleri sayfa üst düzeyine çeker ve hem kaynak kapsamını hem boyama sırasını bozar. İkincisi, içerik XPS ile PDF arası sayfa matrisi zaten uygulanmış olarak yakalanır; bu yüzden Form öğesini yayınlamak o matrisin tersiyle çarpmayı gerektirir, aksi halde her yerleştirme 0.75 ölçeğini ve Y çevirisini yeniden uygular. Form ayrıca kaynaklarına sahip olmalıdır: HotPDF yalnızca yakalanan içerik akışının gerçekten başvurduğu yazı tiplerini, XObject öğelerini, desenleri, ExtGState öğelerini ve renk uzaylarını kopyalar; tüm sayfa kaynak sözlüğünü klonlamak, kaydedilmekte olan Form öğesini kendi kaynak grafiğine sürükler ve bir döngü kurardı. Yazı tipleri sıradan sayfalarda doğrudan sözlükte kalır ve ancak yakalanan içerik gerçekten bir Tf içerdiğinde paylaşılan dolaylı sözlüğe yükseltilir; böylece yeniden kullanılabilir görseli olmayan bir belge bu mekanizmanın bedelini ödemez. Hata bildirmeden önce bilmeye değer bir belirtim sınırı: ECMA-388 bölüm 13.4, bir VisualBrush üzerindeki ViewboxUnits ve ViewportUnits öğelerinin ikisinin de Absolute olmasını ister; bu yüzden göreli birimler eksik özellik değildir — uyumsuz girdidir ve HotPDF onlar için koordinat anlambilimi uydurmayı reddeder

HotPDF bir XPS VisualBrush görsel ağacını bir kez PDF Form XObject olarak derler, yerleştirmeler ölçeği yeniden uygulamasın diye sabit sayfa matrisinin tersi üzerinden yayınlar ve yalnızca yakalanan içeriğin gerçekten başvurduğu kaynakları kopyalar
İçerik, sayfa matrisi zaten uygulanmış olarak yakalanır; bu yüzden Form tersi üzerinden yayınlanır ve yalnızca kendi içerik akışının başvurduğu kaynakları taşır

ImageBrush döşemesi: dört kip, dört hücre boyutu

XPS döşeme kipleri, kapsanan alan boyunca yinelenen görüntü yerleştirmelerine açılmak yerine ISO 32000-1 bölüm 8.7.3 içindeki PDF döşeme desenlerine eşlenir; bu, çıktı boyutunu ve dönüşüm süresini fırçanın sayfanın ne kadarını kapladığından bağımsız tutar. Eşleme, gördüğünüzde mekaniktir: yansıma, bir desen hücresinin içine aynalanmış yerleştirmeler koyarak ve hücreyi ona göre büyüterek ifade edilir

  • Tile — tek yerleştirme, hücre 1×1 görünüm penceresi kalır
  • FlipX — iki yerleştirme, hücre 2×1 boyutuna genişler
  • FlipY — iki yerleştirme, hücre 1×2 boyutuna yükselir
  • FlipXY — dört yerleştirme, hücre 2×2 boyutuna büyür

Her yerleştirme kendi kırpma dikdörtgenini taşır; çünkü alt hücresini aşan bir Viewbox eşlemesi komşu yansımaya taşardı. Desen /Matrix öğesi insanları yakalayan kısımdır. Bir döşeme deseni, desen seçildiği andaki grafik durumuna değil üst içerik akışının varsayılan kullanıcı uzayına bağlanır; bu yüzden matrisin üç katmanı da açıkça bileştirmesi gerekir — sabit sayfa izdüşümü, Path dönüşümü ve fırça yereli Transform — ortamdaki CTM değerine bel bağlamak yerine. HotPDF ayrıca ayırmadan önce doğrular: RegisterImageTilingPattern bir deseni 1.024 yerleştirmeyle sınırlar ve yozlaşmış kırpmaları, tersinmez matrisleri ve geçersiz görüntü dizinlerini reddeder. Bunun arkasındaki genel PDF tarafı modelini istiyorsanız döşeme desenleri ve Pattern renk uzayı temel işleçleri kapsar

// Sabit sayfa izdüşümü desen matrisine katlanır, sonra fırça yereli matris
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);

Radyal gradyan daire değilse ne olur?

XPS, RadialGradientBrush öğesini GradientOrigin, Center, RadiusX ve RadiusY ile tanımlar; bu yüzden fırça bir elipstir. ISO 32000-1 bölüm 8.7.4.5.4 içindeki PDF gölgelendirme tipi 3, iki daire arasında karıştırma yapar ve bir elipsi doğrudan ifade etmenin yolu yoktur. İki yarıçapı tek sayıya ortalamak cazip kestirmedir ve daireye yakın olmayan her fırçada gözle görülür biçimde yanlıştır. HotPDF bunun yerine sorunu koordinat sistemine taşır: Y eksenini RadiusY / RadiusX ile ölçekler, o ölçekli uzayda dürüst bir dairesel gölgelendirme kaydeder, deseni seçer ve ardından yazılacak yol geometrisi hâlâ özgün XPS kullanıcı uzayında olsun diye hemen ters ölçeği yayar

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);   // desen CTM değerini tam burada yakalar
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

O parçacıktaki sıralama numaranın tamamıdır ve üslup değildir. Bir PDF gölgelendirme deseni, geçerli renk olarak seçildiği anda geçerli dönüşüm matrisini yakalar; bu yüzden geçici ölçek SetFillPattern ya da SetStrokePattern işleminden önce yayılmalı, ters ölçek ise seçimi izlemeli ama yol işleçlerinden önce gelmelidir. Sırayı her iki yönde de yanlış yaparsanız ilk yolda doğru işlenen ve sonraki her yolda kayan bir gradyan elde edersiniz. Göreli koordinat kipi için ilgili bir kısıt geçerlidir: RadiusX ve RadiusY, yol genişliği ve yüksekliğine karşı ayrı ayrı çözümlenmelidir; çünkü ikisini tek kenar uzunluğuyla ölçeklemek, kare olmayan her yolda elipsin en boy oranını sessizce değiştirir

HotPDF, eliptik bir XPS RadialGradientBrush öğesini PDF gölgelendirme tipi 3 üzerine eşler: Y eksenini ölçekler, ölçekli uzayda dairesel bir gölgelendirme kaydeder ve ters ölçeği ancak desen geçerli dönüşüm matrisini yakaladıktan sonra yayar
Elips, gölgelendirme yerine koordinat sistemi tarafından yutulur ve geçici ölçek desen seçimini tam olarak o sırayla kuşatmak zorundadır

Dönüşümün sınırları konusunda dürüst olduğu yer

Bazı XPS yapıları yaklaşık olarak dönüştürülür, bazıları hiç dönüştürülmez ve baştan sona tasarım seçimi onları taklit etmek yerine saymaktır. TIFF ve JPEG XR parçaları WIC üzerinden rasterleştirilir ve korunan alfa hakkında hiçbir söz taşımaz; geçerli alfa kanallı PNG ise temel görüntü artı bir /SMask olarak bölünür. Görüntünün doğal boyutu pixel * 96 / DPI olarak türetilir; önce PNG pHYs ya da JPEG JFIF yoğunluğu okunur ve 96 DPI değerine geri düşülür, böylece bozuk bir yoğunluk başlığı keyfi yerine öngörülebilir bir boyuta iner. Çözümlenememiş matris kaynakları, standart dışı göreli dönüşümler, ColorConvertedBitmap, desteklenmeyen gradyan yayılım kipleri ve bozuk geometri, hepsi UnsupportedFeatureCount değerini artırır ve bozuk girdi, sessizce farklı bir çizime bozulmak yerine kapalı başarısız olur

Arşiv amaçlı bir dönüştürücü için yararlı duruş budur: sessizce yaklaşık sonuç üreten bir dönüşüm, hangi dört öğeyi temsil edemediğini söyleyen bir dönüşümden daha kötüdür; çünkü belge bir kayıt sistemine mühürlenmeden önce denetleyecek bir şeyi yalnızca ikincisi verir. XPS ve OpenXPS dönüşümünü belge işlem hattının geri kalanıyla — sayfa birleştirme, yazı tipleri, imzalama, PDF/A çıktısı — birlikte değerlendiriyorsanız, HotPDF Delphi PDF component sayfası tam özellik kümesini ve desteklenen Delphi ile C++Builder sürümlerini listeler