Artykuł techniczny

Gradienty PDF osiowe i radialne w Delphi z PDFium

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

Geometria shadingów osiowych i radialnych PDFium Component dla Delphi: gradient Type 2 miesza StartColor w EndColor wzdłuż linii od x0 y0 do x1 y1 z /Extend true true ciągnącym się poza oba końce, a gradient Type 3 miesza między okręgiem startowym o promieniu r0 a końcowym o promieniu r1
PDFium maluje shading na tym, na co pozwala klip: Coords Type 2 kładą oś mieszania od x0 y0 do x1 y1, Coords Type 3 kładą dwa okręgi, a /Extend [true true] niesie oba kolory końcowe poza geometrię
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ę

Kompilacja gradientu wielostopniowego PDFium Component w Delphi: trzy Stops na pozycjach 0.0, 0.4 i 1.0 stają się funkcją składania Type 3 z jednym segmentem Type 2 na parę sąsiednich stopów, /Bounds [0.4] dla pozycji wewnętrznej i /Encode [0 1 0 1] mapujące każdą poddomenę z powrotem na pełny zakres
Każda sąsiednia para stopów staje się jednym segmentem Type 2 wewnątrz funkcji Type 3; wyprowadzone tablice /Bounds i /Encode to to, co ręcznie pisane funkcje składania zwykle psują

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

Dlaczego wyglądający ważnie shading PDF nic nie rysuje w PDFium: /ShadingType zapisany jako nazwa Axial jest czytany getterem całkowitym jako 0 i operator sh jest pomijany po cichu, a strumień treści trzymający tylko sh nie ma pudełka obejmującego, dopóki jawny klip re W n nie da mu prostokąta
Obie porażki niewidocznego shadingu są w PDFium ciche, więc SaveAsShadingCreated pisze sama całkowity ShadingType i owija każde wywołanie sh klipem re W n z crop boxa

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