Artikel Teknis

XPS dan OpenXPS ke PDF di Delphi: Koordinat dan Brush

HotPDF mengonversi paket XPS dan OpenXPS ke PDF di dalam Delphi dan C++Builder tanpa print driver, memetakan setiap koordinat fixed-page 96-DPI melalui satu matriks halaman skala-0.75 yang membalik Y, menerbitkan setiap VisualBrush sebagai Form XObject bersama, dan mengubah mode tile ImageBrush menjadi pola tiling PDF native alih-alih gambar gambar yang berulang

Skenario yang menyeret kebanyakan toko Windows ke sini membosankan dan tak terhindarkan. Ada yang sudah mencetak ke Microsoft XPS Document Writer — laporan ERP warisan, form bertanda tangan, sepaket statement — dan kebijakan arsip mengatakan PDF. XPS adalah format penangkapan yang sangat bagus dan yang mengerikan untuk diserahkan ke sistem catatan sepuluh tahun lagi. Jadi file spool harus menjadi PDF halaman-per-halaman, dan begitu Anda mulai menulis konverter itu, bagian menariknya bukan XML-nya. XPS dan PDF tidak sepaham soal di mana titik asal berada, berapa nilai satu unit, dan apa yang diperbolehkan menjadi brush

Dari paket ke PDF dalam satu lintasan

Titik masuknya adalah registry document handler, bukan kelas XPS khusus. THPDFDocumentHandlerRegistry.RegisterStandardHandlers memasang handler XPS, EPUB, dan CBZ; pengenalan berbasis konten, jadi paket yang membawa [Content_Types].xml plus setidaknya satu part .fpage mencetak skor 95 bahkan ketika ekstensi file berbohong, sedangkan ekstensi polos .xps atau .oxps hanya mencetak skor 10. Urutan itu penting saat Anda menerima unggahan, karena penyerang yang mengganti nama EPUB menjadi .xps tidak seharusnya mengemudikan pipeline

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 is the honest score for this conversion
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Semua tentang konversi itu dianggarkan sebelum dicoba. THPDFDocumentHandlerOptions.Default menutup entri arsip di 10.000, byte arsip terurai di 1 GiB, rasio kompresi di 200, resource di 4.096, dan halaman di 10.000, serta membawa CancellationToken opsional agar pekerjaan sisi-server bisa dihentikan di tengah paket. Baca Info.UnsupportedFeatureCount setelahnya dan perlakukan nilai non-nol sebagai temuan sungguhan: HotPDF sengaja menghitung apa yang tak bisa ia petakan alih-alih menggambar aproksimasi dan diam tentang itu

Mengapa halaman XPS butuh matriks alih-alih koordinat yang ditulis ulang?

Karena menulis ulang koordinat menghilangkan stack transform. Sebuah FixedPage XPS dispesifikasikan dalam unit 96-DPI dengan titik asal di kiri atas dan Y bertumbuh ke bawah; ruang pengguna PDF adalah 72-DPI dengan titik asal di kiri bawah dan Y bertumbuh ke atas. Perbaikan naifnya adalah mengalikan setiap angka dengan 0.75 dan mengurangi setiap Y dari tinggi halaman saat ditulis. Itu bekerja untuk satu path datar dan runtuh begitu sebuah RenderTransform, Canvas bersarang, atau matriks milik-brush masuk, karena transform-transform itu didefinisikan di ruang XPS dan penulisan ulang per-koordinat Anda telah meninggalkannya. Karena itu HotPDF menjaga proyeksi sebagai matriks dan menyusunnya. HPDFXPSPageMatrix mengembalikan konstanta tetap sekali per halaman, HPDFMultiplyXPSMatrix menggandengkannya dengan transform path yang terakumulasi, dan hasilnya ditulis sebagai satu operator cm sebelum geometri. Data path lalu ditulis dalam angka XPS tanpa modifikasi, dan itulah mengapa sintaks geometri singkatan bisa berbagi parser berbatas yang sama dengan data path SVG — hanya token fill-rule pembuka F0 atau F1 yang ditangani adapter XPS. Jika Anda mengikuti alasan yang sama untuk impor vektor EMF dan WMF, bentuk argumennya akrab: format impor dikonversi lewat matriks, tidak pernah lewat aritmetika pada koordinat daun

HotPDF menyusun matriks halaman XPS tetap dengan transform path terakumulasi sehingga sistem koordinat XPS 96-DPI kiri-atas mencapai ruang pengguna PDF 72-DPI dengan asal di kiri-bawah, ditulis sebagai satu operator cm per visual
Proyeksi tetap menjadi matriks dan disusun dengan setiap transform bersarang, sehingga data path bisa ditulis dalam angka XPS tanpa modifikasi
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // 96-DPI XPS unit to 72-DPI PDF point
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // XPS Y grows down, PDF Y grows up
  Result.E :=  0;
  Result.F := PageHeight;  // PDF page height, in points
end;

// One composed CTM per visual, emitted before any path operator
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Bagaimana memakai ulang VisualBrush tanpa menggambarnya dua kali?

Sebuah VisualBrush mengecat pohon visual arbitrer — anak-anak Canvas, Path, dan Glyphs — ke sebuah wilayah, mungkin berulang melintasi wilayah itu. HotPDF mengompilasi visual itu sekali menjadi Form XObject PDF lalu menempatkannya, strategi resource yang sama yang dijelaskan di impor SVG lewat Form XObject. Dua detail menentukan apakah itu bekerja. Pertama, visual harus disusuri sebagai anak XML langsung: pemindaian datar untuk elemen yang layak-tile menarik visual bersarang ke level atas halaman dan menghancurkan scoping resource sekaligus urutan pengecatan. Kedua, konten ditangkap dengan matriks halaman XPS-ke-PDF sudah diterapkan, jadi menerbitkan Form menuntut perkalian dengan invers matriks itu, jika tidak setiap penempatan menerapkan ulang skala 0.75 dan pembalikan Y. Form juga harus memiliki resource-nya sendiri: HotPDF menyalin hanya font, XObject, pattern, ExtGState, dan ruang warna yang benar-benar direferensikan oleh content stream tangkapannya; mengkloning seluruh kamus resource halaman akan menyeret Form yang sedang didaftarkan ke dalam graf resource-nya sendiri dan membangun siklus. Font tetap di kamus langsung pada halaman biasa dan dipromosikan ke kamus indirect bersama hanya ketika konten terkapur benar-benar memuat Tf, jadi dokumen tanpa visual yang bisa dipakai ulang tak membayar mesinnya. Catat satu batas spesifikasi yang layak diketahui sebelum Anda mengajukan bug: ECMA-388 bagian 13.4 menuntut ViewboxUnits dan ViewportUnits pada VisualBrush sama-sama Absolute, jadi unit relatif bukan fitur yang hilang — itu input yang tak sesuai, dan HotPDF menolak mengarang semantik koordinat untuknya

HotPDF mengompilasi pohon visual XPS VisualBrush sekali menjadi Form XObject PDF, menerbitkannya lewat invers matriks fixed-page agar penempatan tidak menerapkan ulang skala, dan menyalin hanya resource yang benar-benar direferensikan konten terkapur
Konten ditangkap dengan matriks halaman sudah diterapkan, jadi Form diterbitkan lewat inversnya dan hanya membawa resource yang direferensikan content stream-nya sendiri

Tiling ImageBrush: empat mode, empat ukuran sel

Mode tile XPS dipetakan ke pola tiling PDF dari ISO 32000-1 bagian 8.7.3 alih-alih diuraikan menjadi penempatan gambar berulang melintasi area yang dicakup, yang menjaga ukuran output dan waktu konversi tetap independen dari seberapa banyak halaman yang dicakup brush. Pemetaannya mekanis begitu Anda melihatnya: refleksi diekspresikan dengan menaruh penempatan termirror di dalam satu sel pattern dan memperbesar sel agar cocok

  • Tile — satu penempatan, sel tetap 1×1 viewport
  • FlipX — dua penempatan, sel dilebarkan ke 2×1
  • FlipY — dua penempatan, sel ditinggikan ke 1×2
  • FlipXY — empat penempatan, sel diperluas ke 2×2

Setiap penempatan membawa clip rectangle-nya sendiri, karena pemetaan Viewbox yang melampaui sub-selnya akan berdarah ke refleksi tetangga. Pattern /Matrix adalah bagian yang menjebak orang. Pola tiling berjangkar di ruang pengguna bawaan content stream induknya, bukan di graphics state saat pattern dipilih, jadi matriksnya harus menyusun ketiga lapisan secara eksplisit — proyeksi fixed-page, transform Path, dan Transform milik-brush — alih-alih bertumpu pada CTM ambien. HotPDF juga memvalidasi sebelum mengalokasikan: RegisterImageTilingPattern membatasi pola pada 1.024 penempatan dan menolak clip degenerat, matriks tak invertibel, serta indeks gambar tidak valid. Jika Anda ingin model sisi-PDF umum di balik ini, pola tiling dan ruang warna Pattern membahas operator dasarnya

// Fixed-page projection folded into the pattern matrix, then the brush-local one
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);

Apa yang terjadi saat gradien radial bukan lingkaran?

XPS mendefinisikan RadialGradientBrush dengan GradientOrigin, Center, RadiusX, dan RadiusY, jadi brush itu elips. PDF shading tipe 3, di ISO 32000-1 bagian 8.7.4.5.4, membaurkan dua lingkaran dan tak punya cara mengekspresikan elips secara langsung. Merata-ratakan kedua radius menjadi satu angka adalah jalan pintas yang menggoda dan salah yang terlihat pada brush apa pun yang tidak mendekati bulat. HotPDF sebagai gantinya memindahkan masalah ke sistem koordinat: ia menskalakan Y dengan RadiusY / RadiusX, mendaftarkan shading lingkaran yang jujur di ruang terskala itu, memilih pattern, dan segera menulis skala resiprokalnya sehingga geometri path yang ditulis berikutnya tetap berada di ruang pengguna XPS asli

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);   // the pattern captures the CTM right here
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

Urutan dalam potongan itu adalah seluruh triknya, dan itu bukan soal gaya. Pattern shading PDF menangkap current transformation matrix pada saat ia dipilih sebagai warna saat ini, jadi skala sementara harus ditulis sebelum SetFillPattern atau SetStrokePattern, dan resiprokalnya harus mengikuti pemilihan tetapi mendahului operator path. Salah urutan ke arah mana pun, Anda mendapat gradien yang terender benar di path pertama dan melesat di setiap path berikutnya. Kendala terkait berlaku untuk mode koordinat relatif: RadiusX dan RadiusY harus di-resolve terhadap lebar dan tinggi path secara terpisah, karena menskalakan keduanya dengan satu panjang sisi diam-diam mengubah rasio aspek elips pada path non-persegi apa pun

HotPDF memetakan RadialGradientBrush XPS elips ke PDF shading tipe 3 dengan menskalakan Y, mendaftarkan shading lingkaran di ruang terskala, dan menulis skala resiprokal hanya setelah pattern menangkap current transformation matrix
Elips diserap oleh sistem koordinat alih-alih shading-nya, dan skala sementara harus mengapit pemilihan pattern dalam urutan yang persis itu

Di mana konversi jujur tentang batasnya

Beberapa konstruksi XPS dikonversi secara aproksimasi dan beberapa tidak dikonversi sama sekali, dan pilihan desainnya sepanjang jalan adalah menghitungnya alih-alih memalsukannya. Part TIFF dan JPEG XR dirasterisasi lewat WIC dan tak membawa janji apa pun tentang alpha yang terjaga, sementara PNG dengan kanal alpha valid dipecah menjadi gambar basis plus /SMask. Ukuran intrinsik gambar diturunkan sebagai pixel * 96 / DPI, membaca PNG pHYs atau kerapatan JFIF JPEG lebih dulu lalu jatuh ke 96 DPI, jadi header kerapatan yang rusak mendarat di ukuran yang dapat diprediksi alih-alih sewenang-wenang. Resource matriks yang tak teresolve, transform relatif non-standar, ColorConvertedBitmap, mode spread gradien yang tak didukung, dan geometri cacat semuanya menaikkan UnsupportedFeatureCount, dan input cacat gagal tertutup alih-alih terdegradasi menjadi gambar yang berbeda diam-diam

Itu posisi yang berguna bagi konverter arsip: konversi yang diam-diam mengaproksimasi lebih buruk daripada yang memberi tahu Anda empat elemen mana yang tak bisa ia wakili, karena hanya yang kedua memberi Anda sesuatu untuk diperiksa sebelum dokumen disegel ke sistem catatan. Jika Anda mengevaluasi konversi XPS dan OpenXPS bersama sisa pipeline dokumen — komposisi halaman, font, penandatanganan, output PDF/A — halaman komponen PDF Delphi HotPDF mendaftar set fitur penuh dan versi Delphi serta C++Builder yang didukung