Articol tehnic

XPS și OpenXPS în PDF în Delphi: coordonate și pensule

HotPDF convertește pachetele XPS și OpenXPS în PDF în interiorul Delphi și C++Builder fără driver de imprimare, mapând fiecare coordonată de pagină fixă 96-DPI printr-o singură matrice de pagină cu scară 0.75 și inversare Y, publicând fiecare VisualBrush ca un Form XObject partajat și transformând modurile de dală ImageBrush în tiling patterns PDF native în loc de desenuri de imagini repetate

Scenariul care târăște majoritatea firmelor Windows în asta este plictisitor și de neevitat. Ceva se tipărește deja la Microsoft XPS Document Writer — un raport ERP legacy, un formular semnat, un lot de extrase de cont — iar politica de arhivă spune PDF. XPS este un format de captură foarte bun și unul groaznic de dat unui sistem de arhivă peste un deceniu. Deci fișierul spool trebuie să devină un PDF pagină cu pagină, iar în momentul în care începeți să scrieți acel convertor descoperiți că partea interesantă nu este XML-ul. Ci că XPS și PDF nu sunt de acord unde este originea, cât valorează o unitate și ce are voie să fie o pensulă

De la pachet la PDF într-o singură trecere

Punctul de intrare este registrul de handler-e de documente, nu o clasă XPS specială. THPDFDocumentHandlerRegistry.RegisterStandardHandlers instalează handler-ele XPS, EPUB și CBZ; recunoașterea se bazează pe conținut, astfel încât un pachet care poartă [Content_Types].xml plus cel puțin o parte .fpage punctează 95 chiar când extensia fișierului minte, în timp ce o extensie goală .xps sau .oxps punctează doar 10. Ordinea aceasta contează când acceptați încărcări, pentru că un atacator care redenumește un EPUB în .xps nu ar trebui să dirijeze pipeline-ul

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 este scorul onest al acestei conversii
  finally
    Output.Free;
    Registry.Free;
  end;
end;

Tot ce ține de conversie este bugetat înainte să fie încercat. THPDFDocumentHandlerOptions.Default plafonează intrările de arhivă la 10.000, octeții de arhivă extinși la 1 GiB, raportul de comprimare la 200, resursele la 4.096 și paginile la 10.000, și poartă un CancellationToken opțional astfel încât un job pe partea de server poate fi oprit la mijlocul pachetului. Citiți Info.UnsupportedFeatureCount după și tratați o valoare nenulă ca o constatare reală: HotPDF numără în mod deliberat ce nu a putut mapa în loc să deseneze o aproximație și să tacă despre asta

De ce are nevoie o pagină XPS de o matrice în loc de coordonate rescrise?

Pentru că rescrierea coordonatelor pierde stiva de transformări. Un FixedPage XPS este specificat în unități 96-DPI cu originea în stânga sus și Y crescând în jos; spațiul de utilizator PDF este 72-DPI cu originea în stânga jos și Y crescând în sus. Reparația naivă este să înmulțiți fiecare număr cu 0.75 și să scădeți fiecare Y din înălțimea paginii pe măsură ce îl emiteți. Merge pentru o singură cale plată și se destramă în clipa în care intră un RenderTransform, un Canvas imbricat sau o matrice locală de pensulă, pentru că acele transformări sunt definite în spațiul XPS, iar rescrierea dumneavoastră per coordonată l-a părăsit deja. HotPDF păstrează de aceea proiecția ca matrice și o compune. HPDFXPSPageMatrix returnează constantele fixe o dată per pagină, HPDFMultiplyXPSMatrix le concatenează cu transformarea de cale acumulată, iar rezultatul este emis ca un singur operator cm înaintea geometriei. Datele de cale sunt apoi scrise în numere XPS nemodificate, ceea ce explică de ce sintaxa de geometrie abreviată poate împărți același parser delimitat folosit pentru datele de cale SVG — doar tokenul de regulă de umplere F0 sau F1 de la început este tratat de adaptorul XPS. Dacă ați urmărit același raționament pentru importul vectorial EMF și WMF, forma argumentului vă este familiară: formatele de import sunt convertite prin matrice, niciodată prin aritmetică pe coordonate frunză

HotPDF compune matricea fixă de pagină XPS cu transformarea de cale acumulată astfel încât un sistem de coordonate XPS 96-DPI cu originea stânga sus ajunge în spațiul de utilizator PDF 72-DPI cu originea în stânga jos, emis ca un singur operator cm per vizual
Proiecția rămâne o matrice și este compusă cu fiecare transformare imbricată, astfel încât datele de cale pot fi scrise în numere XPS nemodificate
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
  Result.A :=  0.75;       // unitate XPS 96-DPI în punct PDF 72-DPI
  Result.B :=  0;
  Result.C :=  0;
  Result.D := -0.75;       // Y-ul XPS crește în jos, Y-ul PDF crește în sus
  Result.E :=  0;
  Result.F := PageHeight;  // înălțimea paginii PDF, în puncte
end;

// Un singur CTM compus per vizual, emis înaintea oricărui operator de cale
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));

Cum reutilizați un VisualBrush fără să-l desenați de două ori?

Un VisualBrush pictează un arbore vizual arbitrar — copii Canvas, Path și Glyphs — într-o regiune, eventual repetat pe ea. HotPDF compilează acel vizual o dată într-un Form XObject PDF și apoi îl plasează, ceea ce este aceeași strategie de resurse descrisă în importul SVG prin Form XObjects. Două detalii decid dacă merge. Primul, vizualul trebuie parcurs ca copii XML direcți: o scanare plată după elemente demne de dală trage vizualele imbricate la nivelul de sus al paginii și distruge atât scopul resurselor, cât și ordinea de pictare. Al doilea, conținutul este capturat cu matricea de pagină XPS-spre-PDF deja aplicată, astfel încât publicarea Form-ului cere înmulțirea cu inversa acelei matrice, altfel fiecare plasare reaplică scara 0.75 și inversarea Y. Form-ul trebuie să-și și dețină resursele: HotPDF copiază doar fonturile, XObjects, pattern-urile, ExtGStates și spațiile de culoare pe care fluxul de conținut capturat le referă efectiv; clonarea întregului dicționar de resurse al paginii ar trage Form-ul în curs de înregistrare în propriul graf de resurse și ar construi un ciclu. Fonturile rămân într-un dicționar direct pe paginile obișnuite și sunt promovate la un dicționar indirect partajat doar când conținutul capturat conține efectiv un Tf, astfel încât un document fără vizuale reutilizabile nu plătește pentru mecanism. Notați o graniță de specificație demnă de știut înainte să depuneți un bug: ECMA-388 secțiunea 13.4 cere ca atât ViewboxUnits, cât și ViewportUnits pe un VisualBrush să fie Absolute, deci unitățile relative nu sunt o funcționalitate lipsă — sunt date de intrare neconforme, iar HotPDF refuză să inventeze semantică de coordonate pentru ele

HotPDF compilează un arbore vizual VisualBrush XPS o dată într-un Form XObject PDF, îl publică prin inversa matricei fixe de pagină astfel încât plasările să nu reaplice scara, și copiază doar resursele pe care conținutul capturat le referă efectiv
Conținutul este capturat cu matricea de pagină deja aplicată, astfel încât Form-ul este publicat prin inversa ei și poartă doar resursele pe care propriul său flux de conținut le referă

Tiling-ul ImageBrush: patru moduri, patru dimensiuni de celulă

Modurile de dală XPS se mapează pe tiling patterns PDF din secțiunea 8.7.3 ISO 32000-1 în loc să fie extinse în plasări de imagini repetate pe aria acoperită, ceea ce ține dimensiunea ieșirii și timpul de conversie independente de cât de mult din pagină acoperă pensula. Maparea este mecanică odată ce o vedeți: reflexia este exprimată punând plasări în oglindă în interiorul unei singure celule de pattern și mărind celula să se potrivească

  • Tile — o plasare, celula rămâne viewport 1×1
  • FlipX — două plasări, celula lățită la 2×1
  • FlipY — două plasări, celula înălțată la 1×2
  • FlipXY — patru plasări, celula extinsă la 2×2

Fiecare plasare poartă propriul dreptunghi de clip, pentru că o mapare Viewbox care își depășește sub-celula ar sângera în reflexia vecină. /Matrix-ul pattern-ului este partea care prinde lumea. Un tiling pattern este ancorat la spațiul de utilizator implicit al fluxului său de conținut părinte, nu la starea grafică curentă în momentul selectării pattern-ului, astfel încât matricea trebuie să compună explicit toate cele trei straturi — proiecția fixă de pagină, transformarea Path și Transform-ul local al pensulei — în loc să se bazeze pe un CTM ambiental. HotPDF validează și înainte să aloce: RegisterImageTilingPattern limitează un pattern la 1.024 de plasări și respinge clipuri degenerate, matrice neinversabile și indici de imagine invalizi. Dacă vreți modelul general din partea PDF din spatele acestui lucru, tiling patterns și spațiul de culoare Pattern acoperă operatorii de bază

// Proiecția fixă de pagină pliată în matricea pattern-ului, apoi cea locală a pensulei
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);

Ce se întâmplă când un gradient radial nu este un cerc?

XPS definește un RadialGradientBrush cu GradientOrigin, Center, RadiusX și RadiusY, deci pensula este o elipsă. Umbrirea PDF tip 3, în secțiunea 8.7.4.5.4 ISO 32000-1, amestecă între două cercuri și nu are cum să exprime direct o elipsă. Medierea celor două raze într-un singur număr este scurtătura tentantă și este vizibil greșită pe orice pensulă care nu este aproape rotundă. HotPDF mută în schimb problema în sistemul de coordonate: scalează Y cu RadiusY / RadiusX, înregistrează o umbrire circulară onestă în acel spațiu scalat, selectează pattern-ul și emite imediat scara reciprocă astfel încât geometria de cale scrisă apoi să fie tot în spațiul de utilizator XPS original

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);   // pattern-ul capturează CTM-ul chiar aici
if Abs(ScaleY - 1) > 0.000001 then
  Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);

Ordinea din fragmentul acela este tot trucul, și nu este stilistică. Un shading pattern PDF capturează matricea de transformare curentă în momentul în care este ales ca culoare curentă, astfel încât scara temporară trebuie emisă înainte de SetFillPattern sau SetStrokePattern, iar reciprocul trebuie să urmeze selecției dar să preceadă operatorii de cale. Luați ordinea greșit în oricare direcție și aveți un gradient care se randează corect pe prima cale și derivează pe fiecare următoare. O constrângere înrudită se aplică modului de coordonate relative: RadiusX și RadiusY trebuie rezolvate contra lățimii și înălțimii căii separat, deoarece scalarea ambelor cu o singură lungime de muchie schimbă în tăcere raportul de aspect al elipsei pe orice cale non-pătrată

HotPDF mapează un RadialGradientBrush XPS eliptic pe umbrirea PDF tip 3 scalând Y, înregistrând o umbrire circulară în spațiul scalat și emițând scara reciprocă doar după ce pattern-ul a capturat matricea de transformare curentă
Elipsa este absorbită de sistemul de coordonate mai degrabă decât de umbrire, iar scara temporară trebuie să încadreze selecția pattern-ului în exact ordinea aceea

Unde conversia este onestă în privința limitelor ei

Unele construcții XPS sunt convertite aproximativ și unele deloc, iar alegerea de design pretutindeni este să le numere în loc să le falsifice. Părțile TIFF și JPEG XR sunt rasterizate prin WIC și nu poartă nicio promisiune despre alpha păstrat, în timp ce PNG cu un canal alpha valid este împărțit într-o imagine de bază plus un /SMask. Dimensiunea intrinsecă a imaginii este derivată ca pixel * 96 / DPI, citind întâi densitatea PNG pHYs sau JFIF JPEG și căzând înapoi la 96 DPI, astfel încât un antet de densitate stricat aterizează la o dimensiune previzibilă, nu arbitrară. Resursele de matrice nerezolvate, transformările relative nestandard, ColorConvertedBitmap, modurile de răspândire a gradientului nesuportate și geometria malformată incrementează toate UnsupportedFeatureCount, iar datele de intrare malformate eșuează închis în loc să degradeze într-un desen silențios diferit

Aceasta este poziția utilă pentru un convertor de arhivă: o conversie care aproximează în tăcere este mai rea decât una care vă spune care patru elemente nu a putut reprezenta, pentru că doar a doua vă dă ceva de verificat înainte ca documentul să fie sigilat într-un sistem de arhivă. Dacă evaluați conversia XPS și OpenXPS alături de restul pipeline-ului de documente — compoziție de pagini, fonturi, semnare, ieșire PDF/A — pagina HotPDF Delphi PDF component listează setul complet de funcționalități și versiunile suportate Delphi și C++Builder