HotPDF konwertuje pakiety XPS i OpenXPS do PDF w Delphi i C++Builderze bez sterownika drukarki, mapując każdą współrzędną strony stałej 96 DPI przez jedną macierz stron o skali 0.75 z odwróceniem Y, publikując każdy VisualBrush jako współdzielony Form XObject i zamieniając tryby kafelkowania ImageBrush w natywne wzory kafelkowania PDF zamiast powtarzanych rysowań obrazów
Scenariusz, który wciąga w to większość warsztatów pracujących na Windows, jest nudny i nieunikniony. Coś już drukuje się do Microsoft XPS Document Writer — raport ze starego systemu ERP, podpisany formularz, partia wyciągów — a polityka archiwizacji każe mieć PDF. XPS to całkiem dobry format przechwytywania i straszny format do oddania systemowi ewidencyjnemu za dziesięć lat. Plik bufora musi więc stać się PDF-em strona za stroną, a gdy zaczynasz pisać taki konwerter, odkrywasz, że ciekawa część to nie XML. To to, że XPS i PDF nie zgadzają się co do tego, gdzie leży początek układu, ile jest warta jednostka i czym wolno być pędzlowi
Od pakietu do PDF w jednym przebiegu
Punktem wejścia jest rejestr obsług dokumentów, nie specjalna klasa XPS. THPDFDocumentHandlerRegistry.RegisterStandardHandlers instaluje obsługę XPS, EPUB i CBZ; rozpoznawanie opiera się na zawartości, więc pakiet noszący [Content_Types].xml plus co najmniej jedną część .fpage zdobywa 95 punktów nawet wtedy, gdy rozszerzenie pliku kłamie, podczas gdy samo rozszerzenie .xps lub .oxps zdobywa tylko 10. Kolejność ta ma znaczenie, gdy przyjmujesz przesyłane pliki, bo atakujący zmieniający nazwę EPUB-owi na .xps nie powinien sterować potokiem
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 to uczciwa ocena tej konwersji
finally
Output.Free;
Registry.Free;
end;
end;
Wszystko w tej konwersji jest budżetowane, zanim zostanie podjęte. THPDFDocumentHandlerOptions.Default ogranicza wpisy archiwum do 10 000, rozwinięte bajty archiwum do 1 GiB, stopień kompresji do 200, zasoby do 4 096, a strony do 10 000 i niesie opcjonalny CancellationToken, dzięki któremu zadanie po stronie serwera można zatrzymać w połowie pakietu. Potem odczytaj Info.UnsupportedFeatureCount i traktuj wartość niezerową jako realne ustalenie: HotPDF celowo liczy, czego nie potrafił zmapować, zamiast rysować przybliżenie i milczeć o tym
Dlaczego strona XPS potrzebuje macierzy, a nie przepisanych współrzędnych?
Bo przepisywanie współrzędnych gubi stos transformacji. XPS-owy FixedPage jest wyspecyfikowany w jednostkach 96 DPI z początkiem układu w lewym górnym rogu i rosnącym w dół Y; przestrzeń użytkownika PDF to 72 DPI z początkiem w lewym dolnym rogu i rosnącym w górę Y. Naiwna poprawka to pomnożenie każdej liczby przez 0.75 i odjęcie każdego Y od wysokości strony w chwili emitowania. To działa dla jednej płaskiej ścieżki i rozpada się w chwili, gdy pojawia się RenderTransform, zagnieżdżony Canvas albo macierz lokalna pędzla, bo te transformacje są zdefiniowane w przestrzeni XPS, a twój przepis-per-współrzędną już ją opuścił. HotPDF trzyma więc rzut jako macierz i składa go z resztą. HPDFXPSPageMatrix zwraca stałe wartości raz na stronę, HPDFMultiplyXPSMatrix konkatenuje ją z narosłą transformacją ścieżki, a wynik jest emitowany jako pojedynczy operator cm przed geometrią. Dane ścieżki zapisuje się potem w niezmienionych liczbach XPS, dlatego też skrócona składnia geometrii może dzielić ten sam ograniczony parser, co dane ścieżki SVG — tylko wiodący token reguły wypełniania F0 lub F1 obsługuje adapter XPS. Jeśli prowadziłeś to samo rozumowanie przy imporcie wektorowym EMF i WMF, kształt argumentu jest ci znajomy: formaty importowe konwertuje się macierzą, nigdy arytmetyką na współrzędnych liści
function HPDFXPSPageMatrix(PageHeight: Double): THPDFXPSMatrix;
begin
Result.A := 0.75; // jednostka XPS 96 DPI na punkt PDF 72 DPI
Result.B := 0;
Result.C := 0;
Result.D := -0.75; // Y XPS rośnie w dół, Y PDF w górę
Result.E := 0;
Result.F := PageHeight; // wysokość strony PDF, w punktach
end;
// jedna złożona macierz CTM per wizual, emitowana przed operatorami ścieżki
Effective := HPDFMultiplyXPSMatrix(HPDFXPSPageMatrix(PageHeightPDF), PathMatrix);
Page.AppendRawContent(HPDFXPSMatrixOperator(Effective));
Jak ponownie użyć VisualBrush, nie rysując go dwa razy?
VisualBrush maluje dowolne drzewo wizualne — dzieci Canvas, Path i Glyphs — w obszar, możliwie powtarzane w całym obszarze. HotPDF kompiluje ten wizual raz do PDF-owego Form XObject, a potem go umieszcza, co jest tą samą strategią zasobów opisaną w imporcie SVG przez Form XObjects. O tym, czy to działa, decydują dwa szczegóły. Po pierwsze, drzewo wizualne trzeba przejść jako bezpośrednie dzieci XML: płaskie skanowanie w poszukiwaniu elementów nadających się do kafelków wyciąga zagnieżdżone wizualy na szczyt strony i niszczy zarówno zakres zasobów, jak i kolejność malowania. Po drugie, zawartość jest przechwytywana z już nałożoną macierzą strony XPS-do-PDF, więc publikacja Formu wymaga przemnożenia przez odwrotność tej macierzy, inaczej każde umieszczenie nanosi ponownie skalę 0.75 i odwrócenie Y. Form musi też mieć własne zasoby: HotPDF kopiuje wyłącznie czcionki, XObjects, wzory, ExtGStates i przestrzenie barw, do których przechwycony strumień zawartości faktycznie się odwołuje; sklonowanie całego słownika zasobów strony wciągnęłoby rejestrowany Form do jego własnego grafu zasobów i zbudowało cykl. Czcionki zostają w słowniku bezpośrednim na zwykłych stronach i są awansowane do współdzielonego słownika pośredniego dopiero wtedy, gdy przechwycona zawartość naprawdę zawiera Tf, więc dokument bez wizualów do ponownego użycia nie płaci za tę maszynerię. Zanotuj jedną granicę specyfikacji, którą warto znać, zanim zgłosisz błąd: ECMA-388 sekcja 13.4 wymaga, by zarówno ViewboxUnits, jak i ViewportUnits na VisualBrush miały wartość Absolute, więc jednostki względne nie są brakującą funkcją — to niezgodne wejście, a HotPDF odmawia wymyślania im semantyki współrzędnych
Kafelkowanie ImageBrush: cztery tryby, cztery rozmiary komórek
Tryby kafelkowania XPS mapują się na wzory kafelkowania PDF z ISO 32000-1 sekcji 8.7.3, zamiast być rozwijane w powtarzane umieszczenia obrazów na pokrytym obszarze, co utrzymuje rozmiar wyjścia i czas konwersji niezależnymi od tego, ile strony pędzel pokrywa. Mapowanie jest mechaniczne, gdy je zobaczysz: odbicie wyraża się, umieszczając lustrzane umieszczenia wewnątrz jednej komórki wzoru i powiększając komórkę, by to pomieścić
Tile— jedno umieszczenie, komórka zostaje przy viewport 1×1FlipX— dwa umieszczenia, komórka poszerzona do 2×1FlipY— dwa umieszczenia, komórka podwyższona do 1×2FlipXY— cztery umieszczenia, komórka rozwinięta do 2×2
Każde umieszczenie niesie własny prostokąt przycięcia, bo mapowanie Viewbox, które przekroczy swoją sub-komórkę, wyciekłoby do sąsiedniego odbicia. To /Matrix wzoru ludzi zaskakuje. Wzór kafelkowania jest zakotwiczony w domyślnej przestrzeni użytkownika nadrzędnego strumienia zawartości, nie w stanie graficznym bieżącym w chwili wyboru wzoru, więc macierz musi jawnie złożyć wszystkie trzy warstwy — rzut strony stałej, transformację Path i lokalną dla pędzla Transform — zamiast polegać na otaczającym CTM. HotPDF waliduje też przed alokacją: RegisterImageTilingPattern ogranicza wzór do 1 024 umieszczeń i odrzuca zdegenerowane przycięcia, macierze nieodwracalne i nieprawidłowe indeksy obrazów. Jeśli chcesz ogólny model po stronie PDF stojący za tym, wzory kafelkowania i przestrzeń barw Pattern omawia operatory podstawowe
// rzut strony stałej wchodzi do macierzy wzoru, potem ta lokalna dla pędzla
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);
Co się dzieje, gdy gradient radialny nie jest okręgiem?
XPS definiuje RadialGradientBrush z GradientOrigin, Center, RadiusX i RadiusY, więc pędzel jest elipsą. Cieniowanie PDF typu 3, w ISO 32000-1 sekcji 8.7.4.5.4, przechodzi między dwoma okręgami i nie ma sposobu, by wyrazić elipsę wprost. Uśrednienie dwóch promieni do jednej liczby to kuszący skrót i jest widocznie błędne na każdym pędzlu, który nie jest bliski koła. HotPDF przenosi zamiast tego problem do układu współrzędnych: skaluje Y przez RadiusY / RadiusX, rejestruje uczciwe okrągłe cieniowanie w tej przeskalowanej przestrzeni, wybiera wzór i natychmiast emituje skalę odwrotną, by geometria ścieżki zapisana dalej była nadal w pierwotnej przestrzeni użytkownika 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); // wzór przechwytuje CTM właśnie tutaj
if Abs(ScaleY - 1) > 0.000001 then
Page.AppendRawContent('1 0 0 ' + HPDFPDFNumber(1 / ScaleY) + ' 0 0 cm'#10);
Kolejność w tym fragmencie to cały trik i nie jest stylistyczna. Wzór cieniowania PDF przechwytuje bieżącą macierz transformacji w chwili, gdy zostaje wybrany jako bieżący kolor, więc tymczasowa skala musi być wyemitowana przed SetFillPattern lub SetStrokePattern, a odwrotna musi następować po wyborze, ale przed operatorami ścieżki. Pomyl kolejność w którąkolwiek stronę i masz gradient, który renderuje się poprawnie na pierwszej ścieżce i dryfuje na każdej następnej. Pokrewne ograniczenie dotyczy trybu współrzędnych względnych: RadiusX i RadiusY muszą być rozwiązane osobno względem szerokości i wysokości ścieżki, bo przeskalowanie obu jedną długością krawędzi po cichu zmienia proporcje elipsy na każdej niekwadratowej ścieżce
Gdzie konwersja jest szczera co do swoich ograniczeń
Niektóre konstrukcje XPS są konwertowane przybliżenie, a niektóre nie są konwertowane wcale, a decyzją projektową w całym komponencie jest ich liczenie, zamiast ich udawania. Części TIFF i JPEG XR są rastrowane przez WIC i nie niosą obietnicy zachowania alfy, podczas gdy PNG z poprawnym kanałem alfy jest dzielony na obraz bazowy plus /SMask. Wewnętrzny rozmiar obrazu wynika ze wzoru pixel * 96 / DPI, przy czym najpierw czytane są PNG pHYs lub gęstość JFIF z JPEG, a w razie braku przyjmuje się 96 DPI, więc zły nagłówek gęstości ląduje przy przewidywalnym rozmiarze, a nie dowolnym. Nierozwiązane zasoby macierzy, niestandardowe transformacje względne, ColorConvertedBitmap, niewspierane tryby rozprowadzania gradientu i zniekształcona geometria wszystkie zwiększają UnsupportedFeatureCount, a zniekształcone wejście kończy się porażką zamykającą, zamiast degradować do po cichu innego rysunku
Taka jest użyteczna postawa konwertera archiwalnego: konwersja, która po cichu przybliża, jest gorsza od tej, która mówi ci, których czterech elementów nie potrafiła przedstawić, bo tylko druga daje ci coś do sprawdzenia, zanim dokument zostanie zapieczętowany w systemie ewidencyjnym. Jeśli oceniasz konwersję XPS i OpenXPS obok reszty potoku dokumentowego — skład stron, czcionki, podpisywanie, wyjście PDF/A — strona komponentu HotPDF Delphi PDF wylistowuje pełny zestaw funkcji i wspierane wersje Delphi i C++Buildera