PDFium Component tworzy nowe warstwy PDF z Delphi przez TPdf.SaveAsOptionalContentCreated. W jednym zweryfikowanym zapisie metoda dodaje grupy zawartości opcjonalnej (OCG), słowniki przynależności zawartości opcjonalnej (OCMD) i dostarczony przez wołającego rysunek owinięty w Form XObjects, których wpis /OC wskazuje nową grupę, więc PDFium i inne przeglądarki pokazują albo chowają ten rysunek, gdy warstwa jest przełączana
Żądanie rzadko jest abstrakcyjne. Zespół inżynierii zakładu chce wymiarów metrycznych i imperialnych na tym samym arkuszu rysunku, z widocznym dokładnie jednym zestawem. Drukarnia chce nakładki linii cięcia, która widać na ekranie, ale można ją wyłączyć przed odbitką próbną. Żaden z plików nie ma jeszcze żadnych warstw, więc nie ma czego przełączać. Przełączanie istniejących warstw to inna robota, opisana w przełączaniu istniejących warstw zawartości opcjonalnej PDF przez SaveAsOptionalContentConfigured; ten artykuł jest o tworzeniu warstw od zera
Dlaczego nie wystarczy ustawić /OC na obiekcie strony przez PDFium?
PDFium nie ma publicznej funkcji do przyczepiania zawartości opcjonalnej do istniejącego obiektu strony. Publiczne nagłówki nie zawierają FPDFPageObj_SetOC ani niczego równoważnego, więc zwykła droga edycji-i-FPDFPage_GenerateContent nie wyprodukuje treści bramkowanej. Bramka musi zostać zapisana w strukturze pliku, a jedynym pytaniem jest, którą strukturę PDFium uszanuje przy renderowaniu
ISO 32000-1 §8.11.3 daje trzy sposoby uczynienia grafiki opcjonalną, a PDFium traktuje je różnie:
- Sekcje strumienia treści strony owinięte w marked content
/OC /Name BDC...EMC, sprawdzane per obiekt strony podczas renderowania - Form XObjects i image XObjects niosące wpis
/OCwe własnym słowniku, sprawdzane, gdy XObject jest malowany - Adnotacje z wpisem
/OCw słowniku adnotacji, których ścieżka renderowania PDFium w ogóle nie konsultuje, więc bramkowana adnotacja zostaje widoczna niezależnie od stanu warstwy
Owijanie w marked content znaczy przepisywanie istniejących strumieni treści operator po operatorze, a adnotacje wyklucza renderer. Zostaje droga Form XObject i to nią idzie PDFium Component: twój rysunek wchodzi do nowego Form XObject z /OC wskazującym grupę, forma jest rejestrowana w słowniku /Resources /XObject strony, a nowy strumień treści trzymający pojedyncze /Name Do jest doklejany do /Contents strony. Istniejąca treść strony nigdy nie jest parsowana ani przepisywana
Opisywanie grup i bramkowanych form w Delphi
TPdf.SaveAsOptionalContentCreated przyjmuje trzy tablice i zwraca raport. Oba przeciążenia, jedno piszące do TStream, a jedno do nazwy pliku, dzielą tę samą listę parametrów: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport i opcjonalne AllowSignedDocument: Boolean = False. Metoda dotarła w PDFium Component v3.154.0
Każdy TPdfOptionalContentCreateGroup ma Name, opcjonalne Creator, Intent z pociView albo pociDesign, flagę InitialStateVisible, flagę Locked i RadioTag. Grupy dzielące ten sam RadioTag lądują w jednej tablicy /RBGroups, po której przeglądarka wie, że włączenie jednej wyłącza pozostałe. Każdy TPdfOptionalContentCreateForm nazywa swoją grupę bramkującą przez GroupIndex (indeks liczony od zera w twojej tablicy Groups), PageNumber liczony od 1, surowe bajty strumienia treści, pudełko obejmujące, opcjonalną macierz i opcjonalne ResourceName
var
Pdf: TPdf;
Groups: TPdfOptionalContentCreateGroups;
Forms: TPdfOptionalContentCreateForms;
Report: TPdfOptionalContentCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'sheet-a1.pdf';
Pdf.Active := True;
SetLength(Groups, 2); // SetLength zeruje rekordy
Groups[0].Name := 'Dimensions (mm)';
Groups[0].Intent := pociView;
Groups[0].InitialStateVisible := True;
Groups[0].RadioTag := 'Units';
Groups[1].Name := 'Dimensions (in)';
Groups[1].Intent := pociView;
Groups[1].InitialStateVisible := False;
Groups[1].RadioTag := 'Units';
SetLength(Forms, 2);
Forms[0].GroupIndex := 0;
Forms[0].PageNumber := 1;
Forms[0].Content := TEncoding.ASCII.GetBytes(
'0 0 1 RG 1.5 w 72 120 m 540 120 l S');
Forms[0].BBoxRight := 612;
Forms[0].BBoxTop := 792;
Forms[0].ResourceName := 'DimMetric';
Forms[1] := Forms[0];
Forms[1].GroupIndex := 1;
Forms[1].Content := TEncoding.ASCII.GetBytes(
'1 0 0 RG 1.5 w 72 140 m 540 140 l S');
Forms[1].ResourceName := 'DimImperial';
if not Pdf.SaveAsOptionalContentCreated('sheet-a1-layers.pdf',
Groups, nil, Forms, Report) then
raise Exception.Create(Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Zauważ, że treść formy to czysty rysunek wektorowy. Komponent pisze wyłącznie pusty słownik /Resources na każdej formie, więc strumień treści musi być samowystarczalny: kolory urządzenia, ścieżki, przycinanie i transformacje działają, ale cokolwiek potrzebującego nazwanego zasobu (font dla Tf, obraz dla Do, ExtGState dla gs) nie ma przy czym rozwiązać. Jeśli zostawisz ResourceName puste, formy są nazywane FmOc1, FmOc2 i tak dalej; własna nazwa może zawierać tylko litery, cyfry, podkreślenie, minus i kropkę
Co OCMD dokłada ponad zwykłą grupę?
Słownik przynależności zawartości opcjonalnej czyni widoczność zależną od kilku grup naraz. ISO 32000-1 §8.11.2.2 definiuje dwie formy: listę /OCs z polityką /P, jak /AnyOn albo /AllOn, i wyrażenie widoczności /VE zbudowane w specyfikacji z /And, /Or i /Not. PDFium Component przyjmuje TPdfOptionalContentCreateMembership z MemberGroupIndices, Policy i Expression; gdy Expression.GroupIndices nie jest puste, wyrażenie wygrywa i /VE jest pisane obok /OCs
procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
SetLength(Memberships, 2);
// Widoczne, gdy dowolna warstwa jednostek jest włączona: /OCs [...] /P /AnyOn
SetLength(Memberships[0].MemberGroupIndices, 2);
Memberships[0].MemberGroupIndices[0] := 0;
Memberships[0].MemberGroupIndices[1] := 1;
Memberships[0].Policy := ocmpAnyOn;
// Widoczne tylko, dopóki warstwa imperialna jest wyłączona: /VE [ /Not ref ]
SetLength(Memberships[1].MemberGroupIndices, 1);
Memberships[1].MemberGroupIndices[0] := 1;
Memberships[1].Expression.Kind := poceNot;
SetLength(Memberships[1].Expression.GroupIndices, 1);
Memberships[1].Expression.GroupIndices[0] := 1;
end;
Wspierany podzbiór jest celowo wąski. Polityki są ograniczone do ocmpAllOn i ocmpAnyOn; ocmpAnyOff i ocmpAllOff są odrzucane. Wyrażenia są jeden poziom głębokie, z poceAll, poceAny albo poceNot nad utworzonymi grupami, a /Not wymaga dokładnie jednego indeksu grupy. poceAll serializuje się jako /And, poceAny jako /Or a poceNot jako /Not, jedyne trzy operatory, które §8.11.2.2 definiuje. Wersje przed 3.181.2 pisały /All i /Any, które PDFium ocenia jako ukryte, więc jeśli tworzyłeś OCMD starszą budową, odtwórz je, zamiast ufać wyrażeniu. Każde przynależenie potrzebuje też co najmniej jednego wpisu w MemberGroupIndices, nawet gdy wyrażenie jest obecne, bo ta lista staje się tablicą /OCs. Numery obiektów nowych słowników wracają w Report.CreatedMembershipObjectNumbers
Dwa zachowania warto znać, zanim zbudujesz na OCMD. Po pierwsze, bramkowane formy w tym samym wywołaniu referują wyłącznie grupy: TPdfOptionalContentCreateForm.GroupIndex indeksuje Groups, nie Memberships, więc forma nie da się zbramkować świeżo utworzonym OCMD przez to API. Po drugie, PDFium Component dokłada każdy nowy OCMD do tablicy /OCProperties /OCGs katalogu, podczas gdy /ON, /OFF i /Order zostają tylko dla grup. ISO 32000-1 §8.11.4.2 opisuje /OCGs jako listę grup, ale bez tej rejestracji TPdf.InspectOptionalContent w ogóle nie wylicza przynależenia. Skutkiem ubocznym jest to, że inwentarz wyjścia wypisuje każdy OCMD jako dodatkowy nienazwany wpis w Inventory.Groups, więc dopasowuj grupy po Name, a nie po pozycji
Jak komponent łata istniejące /OCProperties?
TPdf.SaveAsOptionalContentCreated buduje /OCProperties od zera albo rozszerza to, które już jest w katalogu, i rozszerza przez doklejanie, nigdy przez przebudowę. Dokument bez warstw dostaje nowy słownik z /OCGs i domyślną konfiguracją /D niosącą /ON, /OFF i /Order, plus /Locked i /RBGroups, gdy któraś grupa o nie prosi, jak rozkłada to ISO 32000-1 §8.11.4.3
Dla dokumentu, który już ma warstwy, każda docelowa tablica jest łatana przez wstawienie nowych referencji tuż przed jej zamykającym nawiasem. Brzmi to pedanteryjnie i jest z powodu. Tablica /Order może trzymać zagnieżdżone tablice i etykiety tekstowe, które kształtują drzewo warstw pokazywane przez przeglądarkę, a wcześniejsze podejście, które spłaszczało i przebudowywało tablicę, gubiło te etykiety. Wstawienie na końcu trzyma istniejące drzewo nietknięte i po prostu wypisuje nowe grupy po nim. Tablice mieszkające we własnych obiektach pośrednich są rozwiązywane i łatane na miejscu
Od v3.175.0 sama domyślna konfiguracja też może być pośrednia. Niektórzy producenci piszą /OCProperties << /OCGs [...] /D 12 0 R >>, a PDFium Component podąża teraz za tą referencją i łata słownik docelowy, zamiast zawodzić błędem zniekształconego wpisu. Bezpośrednie i pośrednie słowniki /Resources stron, łącznie z pośrednim podsłownikiem /XObject, są traktowane tak samo
Dowodzenie, że nowe warstwy faktycznie się przełączają
Warstwa, która istnieje w /OCProperties, ale niczego nie bramkuje, jest łatwa do wyprodukowania i trudna do zauważenia. Podczas rozwoju pierwsza budowa writera form owijała obiekt strumienia, który już niósł własny nagłówek N 0 obj i trailer endobj, w drugi nagłówek. PDFium nie umiał sparsować zdwojonego obiektu i zamiast zgłosić błąd po cichu pomijał operator Do. Plik się otwierał, panel warstw wyglądał dobrze, a bramkowany rysunek nigdy się nie pojawiał. Obnażyła go sonda, która przełączała warstwę i porównywała wyrenderowane piksele: bez różnicy, więc coś było nie tak
Dlatego metoda weryfikuje przed wydaniem. Wyjście jest otwierane ponownie przez ścisłe wczytanie sprawdzające tabelę odwołań krzyżowych, a każda żądana grupa jest czytana z powrotem przez InspectOptionalContent, by potwierdzić jej nazwę, stan początkowy i rejestrację zablokowania. Dopiero potem wyjście jest kopiowane do twojego strumienia, a wariant plikowy pisze plik tymczasowy i wymienia cel atomowo. Ten sam test przełączania możesz puścić sam, otwierając wyjście ponownie, znajdując grupę po nazwie i zapisując kopię z przeciwnym stanem
function SaveImperialPreset(const SourceName, TargetName: string): Boolean;
var
Pdf: TPdf;
Inventory: TPdfOptionalContentInventory;
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
CfgReport: TPdfOptionalContentConfigureReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourceName;
Pdf.Active := True;
if not Pdf.InspectOptionalContent(Inventory) then
raise Exception.Create(Inventory.ErrorMessage);
for I := 0 to High(Inventory.Groups) do
if (Inventory.Groups[I].Name = 'Dimensions (mm)') or
(Inventory.Groups[I].Name = 'Dimensions (in)') then
begin
SetLength(Changes, Length(Changes) + 1);
Changes[High(Changes)].GroupObjectNumber := Inventory.Groups[I].ObjectNumber;
Changes[High(Changes)].Visible := Inventory.Groups[I].Name = 'Dimensions (in)';
end;
Options := TPdfOptionalContentConfigureOptions.Default;
Result := Pdf.SaveAsOptionalContentConfigured(TargetName, Changes,
Options, CfgReport);
finally
Pdf.Free;
end;
end;
Zrenderuj stronę pierwszą obu plików, a niebieska i czerwona linia powinny się zamienić. Jeśli się nie zamieniają, obejrzyj najpierw wartość EffectiveVisible grupy w inwentarzu; odzwierciedla ona stan, który PDFium faktycznie zrenderuje po nałożeniu tablic konfiguracji i reguł użycia
Limity i pułapki do obwieścia planem
TPdf.SaveAsOptionalContentCreated to ograniczony profil, a większość zaskoczeń bierze się z traktowania go jak ogólnego edytora warstw. Ograniczenia niżej są egzekwowane z góry, więc naruszające wywołanie zawodzi z ustawionym Report.ErrorMessage i nie pisze żadnego wyjścia:
- Potrzebna jest co najmniej jedna grupa; grupy, przynależenia i formy mają limit 4096 wpisów każde, nazwy grup muszą być unikalne i mieć od 1 do 255 znaków, a każda forma przyjmuje najwyżej 16 MiB bajtów treści
- Zaszyfrowane dokumenty są odrzucane, a podpisany dokument potrzebuje
AllowSignedDocument = True; wyjście to pełne przepisanie, więc istniejące podpisy przestaną się walidować - Każdy wpis
Formsstaje się własnym Form XObject, więc włożenie tej samej nakładki na 200 stron przechowuje treść 200 razy, a nie raz - Nowy strumień
Dojest doklewany po istniejącej treści strony, więc biegnie w takim stanie graficznym, w jakim zostawiły go wcześniejsze strumienie; dobrze uformowane strony przywracają swój stan, ale strona z niezbalansowanymq/Qmoże przesunąć twoją nakładkę - Istniejąca treść strony nigdy nie jest przenoszona do warstwy; bramkowany jest tylko rysunek, który podasz w
Forms
Punkt powtarzania to ten, który trzeba zważyć dla dużych dokumentów. Jeśli nakładka jest ciężka i powtarza się na każdej stronie, podejście współdzielonych obiektów z wielorazowych stempli stron zbudowanych na Form XObjects trzyma jedną kopię w pliku, kosztem obsłużenia bramki warstwy samemu
FAQ
Czy PDFium Component włoży istniejącą treść strony do nowej warstwy?
Nie. TPdf.SaveAsOptionalContentCreated bramkuje wyłącznie treść, którą podasz w Forms; nie owija istniejących operatorów w marked content ani nie przenosi ich do XObject. Żeby warstwować istniejącą grafikę, odtwórz ją jako samowystarczalną treść formy i usuń pierwotny rysunek osobnym krokiem
Dlaczego moje bramkowane adnotacje są wciąż widoczne, gdy warstwa jest wyłączona?
PDFium nie sprawdza wpisu /OC słowników adnotacji na swojej ścieżce renderowania, więc bramkowanie na poziomie adnotacji nie ma efektu w przeglądarkach opartych na PDFium. Dlatego PDFium Component przyczepia treść warstw przez Form XObjects, które PDFium faktycznie bramkuje
Jakich numerów obiektów używać przy późniejszym przełączaniu nowych warstw?
Report.CreatedGroupObjectNumbers trzyma numery w pliku, który właśnie został zapisany. Gdy otwierasz ten plik ponownie i konfigurujesz go, bierz ObjectNumber z TPdf.InspectOptionalContent i dopasowuj po Name, co pozostaje poprawne nawet, jeśli późniejszy zapis przenumeruje obiekty
PDFium Component wysyła SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured i InspectOptionalContent jako jeden zestaw narzędzi zawartości opcjonalnej dla Delphi i C++Builder, zbudowany na renderze PDFium. Szczegóły i pobrania są na stronie produktu PDFium Component