PDFium Component tworzy gradienty PDF osiowe i radialne w Delphi przez TPdf.SaveAsShadingCreated. Każdy gradient opisujesz w rekordzie TPdfShadingCreate (typ, przestrzeń barw, współrzędne, kolory albo stopu, strona docelowa), a metoda zapisuje zweryfikowaną kopię, w której każde wypełnienie to prawdziwy słownik shadingu ISO 32000-1, zarejestrowany w słowniku /Resources /Shading strony i malowany operatorem sh, więc PDFium renderuje go natywnie
Potrzeba wychodzi w generatorach raportów i certyfikatów. Projektant podaje mokap z miękkim pasem nagłówka od niebieskiego do białego albo radialną poświatą za pieczęcią, a API obiektów strony pokrywające tekst, ścieżki i obrazy nie ma na to nic. Możesz podrobić gradient setką cienkich prostokątów w ledwo różniących się kolorach, ale wynik pasmuchuje widocznie przy przybliżeniu, nadmuchuje strumień treści i źle się drukuje. PDF ma gładkie shadingi od 1.3; robotą jest zapisanie ich tak, żeby renderer faktycznie je narysował
Dlaczego PDFium Component zapisuje gradienty osobnym zapisem?
TPdf.SaveAsShadingCreated to operacja zapisu, a nie wywołanie obiektu strony, bo publiczne ABI PDFium nie ma funkcji, która tworzy słownik shadingu albo emituje operator sh. Warstwa obiektów strony używana przy tworzeniu PDF-ów od zera z PDFium Component buduje ścieżki, tekst i obrazy i tam się kończy. Shadingi mieszkają poziom niżej, w grafie obiektów i składni strumienia treści
Metoda działa więc etapami. PDFium najpierw zapisuje otwarty dokument natywnie, co zachowuje każdą edycję zrobioną zwykłym API. PDFium Component przepisuje potem ten przygotowany wynik: alokuje jeden obiekt pośredni na shading, dokłada wpis zasobu do każdej strony docelowej, dokleja nowy strumień treści do /Contents strony i zapisuje świeżą tabelę odwołań krzyżowych. Zanim cokolwiek dotrze do twojego strumienia albo pliku, wynik jest wczytywany ponownie ze ścisłą kontrolą odwołań krzyżowych, a metoda zwraca False z komunikatem w raporcie, gdy to ponowne wczytanie zawodzi. Wariant plikowy pisze najpierw do pliku tymczasowego, więc nieudany zapis nie zamazuje istniejącego celu
Budowa dwukolorowego gradientu osiowego
Shading osiowy (ISO 32000-1 §8.7.4.5.3, Type 2) miesza kolory wzdłuż linii od x0 y0 do x1 y1, więc Coords bierze cztery wartości. Shading radialny (§8.7.4.5.4, Type 3) miesza między dwoma okręgami i bierze sześć: x0 y0 r0 x1 y1 r1. Przy gołych StartColor i EndColor PDFium Component emituje wykładniczą funkcję Type 2 (§7.10.3) z /N 1, czyli prostą interpolację liniową, i ustawia /Extend [true true], żeby końcowe kolory ciągnęły się poza oba końce osi
uses
System.SysUtils, PDFium, FPdfCompress;
procedure AddHeaderBand(const InFile, OutFile: string);
var
Pdf: TPdf;
Fills: TPdfShadingCreateShadings;
Report: TPdfShadingCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InFile;
Pdf.Active := True;
SetLength(Fills, 1);
Fills[0] := Default(TPdfShadingCreate);
Fills[0].ShadingType := pstAxial;
Fills[0].ColorSpace := pscDeviceRGB;
Fills[0].Coords := [0, 792, 0, 640]; // x0 y0 x1 y1, od góry do dołu
Fills[0].StartColor := [0.12, 0.35, 0.75]; // ciemny niebieski przy górnej krawędzi
Fills[0].EndColor := [1, 1, 1]; // biel niżej
Fills[0].PageNumber := 1; // liczony od 1
Fills[0].ResourceName := 'HeaderBand'; // opcjonalna, domyślnie Sh1, Sh2...
if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Liczba składowych każdego koloru musi pasować do przestrzeni barw: jedna dla pscDeviceGray i pscCalGray, trzy dla pscDeviceRGB, pscCalRGB i pscLab, cztery dla pscDeviceCMYK. Dla pscICCBased liczba pochodzi z bajtów profilu, które podajesz w IccProfile, a które muszą wystawiać 1, 3 albo 4 składowe; profil jest wbudowywany jako własny strumień ICC z pasującym /N. Przestrzenie skalibrowane przyjmują opcjonalne CalWhitePoint (trzy wartości, dodatnie Y), CalGamma (jedna wartość dla CalGray, trzy dla CalRGB) i, dla Lab, sześciowartościowe LabRange. Każda współrzędna i składowa jest sprawdzana na NaN i nieskończoność, zanim zostanie zapisany bajt
Zauważ, że współrzędne są w domyślnej przestrzeni użytkownika, z początkiem układu w lewym dolnym rogu strony. Pamiętaj, że oś definiuje tylko, gdzie dzieje się mieszanie: malowany obszar to to, na co pozwala klip, co znaczy więcej, niż brzmi, jak pokażą następne sekcje
Gradienty wielostopniowe kompilują się do funkcji składania Type 3
PDFium Component zamienia tablicę Stops w funkcję, jakiej oczekuje czytnik PDF: dwa stopu stają się pojedynczą funkcją Type 2, a 3-256 stopów staje się funkcją składania Type 3 (ISO 32000-1 §7.10.4) złożoną z jednego liniowego segmentu Type 2 na parę sąsiednią. Każdy TPdfShadingCreateStop niesie Position i Color. Gdy Stops jest niepuste, wymienia w całości StartColor i EndColor
Funkcja składania potrzebuje trzech tablic, które łatwo subtelnie spieprzyć ręcznie. /Functions trzyma segmenty, /Bounds trzyma wyłącznie wewnętrzne pozycje stopów (k segmentów potrzebuje k-1 granic), a /Encode mapuje każdą poddomenę z powrotem na [0 1], żeby każdy segment widział swój pełny zakres wejścia. Komponent wyprowadza wszystkie trzy z twoich stopów, dlatego jest surowy dla wejścia: pozycje muszą być skończone, ściśle rosnące, startować od 0 i kończyć na 1 (w granicy 1e-6). Lista stopów startująca od 0.1 zostawiłaby część domeny bez zdefiniowanej funkcji i metoda ją odrzuca, zamiast zgadywać
function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
I: Integer;
begin
Result.Position := Position;
SetLength(Result.Color, Length(Rgb));
for I := 0 to High(Rgb) do
Result.Color[I] := Rgb[I];
end;
procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
Fill := Default(TPdfShadingCreate);
Fill.ShadingType := pstRadial;
Fill.ColorSpace := pscDeviceRGB;
// x0 y0 r0 x1 y1 r1: punkt w środku pieczęci rosnący do okręgu 90 pt
Fill.Coords := [450, 160, 0, 450, 160, 90];
Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
Stop(0.4, [0.95, 0.75, 0.30]),
Stop(1.0, [1.00, 1.00, 1.00])];
Fill.PageNumber := 1;
end;
Dla gradientu trzystopniowego wychodzi /Functions z dwoma segmentami Type 2, /Bounds [0.4] i /Encode [0 1 0 1]. Granice segmentów to twarde złącza kolorów, więc lista stopów nie wygładza przez nie; jeśli złącze wygląda zbyt ostro, dokładaj stopów, zamiast oczekiwać, że czytnik zainterpoluje krzywiznę
Dlaczego PDFium po cichu nic nie rysuje?
PDFium renderuje świeżo utworzony shading tylko, gdy dwa detale są dobrze: /ShadingType musi być liczbą całkowitą, a wywołanie sh musi siedzieć w ścieżce przycinającej. Pomyłka w którymkolwiek i plik się otwiera, waliduje strukturalnie i pokazuje nietkniętą stronę. Żadnego błędu, żadnego ostrzeżenia, żadnej linii w logu
Pułapka pierwsza: /ShadingType to liczba, nie nazwa
ISO 32000-1 tabela 78 definiuje /ShadingType jako liczbę całkowitą: 2 dla osiowego, 3 dla radialnego. Pierwsza wersja tego writera emitowała /ShadingType /Axial, bo tak lepiej się czytało w zrzucie hex. PDFium czyta klucz getterem całkowitym, dostaje 0 dla nazwy, klasyfikuje shading jako nieważny, zawodzi przy jego wczytaniu i pomija operator sh bez skargi. Strona renderowała się pusta i nic w PDFium nie mówiło dlaczego. Przyczyna stała się widoczna dopiero po zbudowaniu minimalnego ręcznie napisanego PDF i otwarciu go w drugim, niezależnym renderze, który zgłosił nieznany typ shadingu 0. Nauka uogólnia się na każdego writera PDF: gdy specyfikacja typuje wyliczenie jako liczbę całkowitą, pisz liczbę całkowitą, jakkolwiek kusi samaopisująca nazwa
Pułapka druga: strumień treści trzymający tylko sh nie ma pudełka obejmującego
Operator sh (ISO 32000-1 §8.7.4.2) maluje shading nad bieżącym regionem przycinania, więc własny zasięg strony ogranicza go na papierze. PDFium buduje jednak obiekt strony shadingu, którego prostokąt pochodzi albo z aktywnej ścieżki klipu, albo z pudełka obejmującego nagromadzonego przez wcześniejsze operatory rysujące w tym samym strumieniu, a renderer wyrzuca obiekty z pustym prostokątem. Świeżo doklejony strumień zawierający nic poza /Sh1 sh nie ma ani jednego, ani drugiego, więc gradient jest wyrzucany przed rasteryzacją. TPdf.SaveAsShadingCreated czyta więc crop box każdej strony (z cofnięciem do media box, a potem do rozmiaru strony) i owija każde wywołanie jawnym klipem re W n. Na stronie US Letter z początkiem układu w zerze doklejony strumień wygląda tak:
q
0 0 612 792 re W n
/HeaderBand sh
Q
Para q/Q izoluje klip i każdą transformację od istniejącej treści strony, więc nic narysowanego później na stronie ich nie odziedziczy. Jeśli sam zapisujesz wywołania shadingu w innym narzędziu, skopiuj ten kształt: jawny klip w tym samym strumieniu jest tani i usuwa zależność od tego, co przyszło wcześniej
Jak własna macierz gradientu współpracuje z klipem?
Macierz gradientu w PDFium Component jest nakładana jako operator cm wstawiony po klipie i przed sh, więc klip zostaje pełnostronicowy, a przekształcana jest wyłącznie geometria gradientu. Ustaw HasMatrix na True i wypełnij MatrixA przez MatrixF, te same sześć liczb co każda macierz transformacji PDF. Wartości muszą być skończone, a wyznacznik niezerowy, bo osobliwa macierz zapadłaby shading w linię. Komponent zapisuje też te sześć wartości jako /Matrix w słowniku shadingu, ale przy bezpośrednim wywołaniu sh to cm w strumieniu treści ustawia układ współrzędnych, w którym maluje PDFium
Kolejność ma tu znaczenie i to trzecia pułapka. Operandy re są interpretowane w bieżącej macierzy transformacji. Daj cm najpierw, a prostokąt klipu wielkości strony zostanie przeskalowany, obrócony albo ściśnięty razem z gradientem, więc macierz, która spłaszcza elipsę, spłaszcza też widoczny obszar i części strony tracą wypełnienie. Najpierw przycinanie w nieprzekształconej przestrzeni użytkownika, potem sklejenie macierzy daje oczekiwany wynik: przekształcony gradient pokrywający całą stronę. Przewodnik po stemplach Form XObject i ich macierzy sześciu liczb przeprowadzi cię przez to, co robi każda składowa macierzy, gdy musisz takową zbudować
procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
BuildSealGlow(Fill); // wypełnienie radialne wycentrowane na 450,160
Fill.Coords := [0, 0, 0, 0, 0, 90]; // okręgi w początku układu
Fill.HasMatrix := True;
Fill.MatrixA := 1.6; // rozciągnij poziomo
Fill.MatrixB := 0;
Fill.MatrixC := 0;
Fill.MatrixD := 0.8; // ściśnij pionowo
Fill.MatrixE := 450; // potem przenieś do środka pieczęci
Fill.MatrixF := 160;
end;
Czytanie raportu i znajomość granic
TPdfShadingCreateReport mówi ci, co zostało zapisane: SourceByteCount i OutputByteCount, CreatedShadingObjectNumbers (jeden numer obiektu na wpis wejściowy, po kolei), AttachedShadingCount, Succeeded i ErrorMessage. Błędy walidacji nazywają wpis i pole, na przykład listę stopów nieściśle rosnącą, więc warto je logować dosłownie
if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
Writeln(Format('%d shading(s) attached, %d -> %d bytes',
[Report.AttachedShadingCount, Report.SourceByteCount,
Report.OutputByteCount]))
else
Writeln('Rejected: ' + Report.ErrorMessage);
Profil jest celowo ograniczony. Tworzy wyłącznie shadingi osiowe i radialne; shadingi funkcyjne i siatkowe (Type 1 oraz 4-7) i przestrzenie barw wzorca są odrzucane. Jedno wywołanie przyjmuje 1-4096 wypełnień, numery stron muszą istnieć, a nazwy zasobów są ograniczone do 127 znaków liter, cyfr, podkreślenia, minusa i kropki. Zaszyfrowane wejście jest odmawiane na etapie trailera. Podpisany dokument potrzebuje AllowSignedDocument := True, a ponieważ wyjście to pełne przepisanie, a nie aktualizacja przyrostowa, istniejące podpisy przestaną się walidować, więc podpisuj po dodaniu gradientów. Każde wypełnienie jest malowane na końcu treści strony, na wierzchu tego, co już tam jest; dla efektu tła utwórz gradient najpierw i dodaj tekst i obrazy potem, albo użyj zawartości opcjonalnej, jak opisuje artykuł o sterowaniu warstwami zawartości opcjonalnej PDF w Delphi, gdy wypełnienie ma być przełączalne
FAQ
Czy SaveAsShadingCreated wspiera gradienty wielostopniowe?
Tak. Podaj 2-256 wpisów w Stops z pozycjami ściśle rosnącymi od 0 do 1. Dwa stopu produkują funkcję Type 2, a trzy albo więcej funkcję składania Type 3 z wyprowadzonymi tablicami /Bounds i /Encode
Dlaczego mój gradient jest niewidoczny w PDFium, gdy plik wygląda ważnie?
Sprawdź, że /ShadingType to liczba całkowita 2 albo 3, a nie nazwa, i że operator sh biegnie wewnątrz klipu re W n w tym samym strumieniu treści. PDFium po cichu pomija nieważne typy shadingów i wyrzuca obiekty shadingów z pustym prostokątem
Jakich przestrzeni barw może użyć utworzony shading?
TPdfShadingCreate przyjmuje DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab i ICCBased z profilem o 1, 3 albo 4 składowych. Przestrzenie barw Pattern i Indexed nie są częścią profilu tworzenia
Tworzenie gradientów, zawartość opcjonalna i reszta etapowanych writerów są z PDFium Component for Delphi and C++Builder, które opakowuje PDFium dla projektów Delphi, C++Builder i Lazarus