PDFium Component steruje warstwami opcjonalnej treści PDF (OCG) w Delphi przez dwie metody TPdf: InspectOptionalContent wylicza każdą warstwę razem z widocznością, którą PDFium faktycznie wyrenderuje, a SaveAsOptionalContentConfigured zapisuje zweryfikowaną kopię, w której wybrane warstwy są włączone albo wyłączone. Ta druga metoda neutralizuje też reguły Usage i /AS, które inaczej cicho cofnęłyby twoją edycję. Obie działają na dokumencie już otwartym w TPdf, więc nie ma drugiego parsera do trzymania w zgodzie z tym, co pokazuje przeglądarka
Zapytanie zwykle przychodzi od firmy z branży CAD albo GIS: zestaw rysunków wychodzi z wymiarami, adnotacjami i tabliczką tytułową na osobnych warstwach, a klient chce kopię z ukrytymi wymiarami, zanim pójdzie do dostawcy. PDFium renderuje zawartość opcjonalną poprawnie, ale jego publiczne ABI nie ma funkcji do wyliczania OCG-ów, wyboru konfiguracji ani przestawienia stanu warstwy. Więc schodzisz na poziom obiektów, edytujesz /OCProperties, zapisujesz, wczytujesz ponownie — a warstwa wciąż tam jest. Powodem jest logika widoczności PDFium i warto ją zrozumieć, zanim dotkniesz jakiegokolwiek bajta
Dlaczego edycja /ON i /OFF nie zmienia tego, co renderuje PDFium?
Edycja tablic /ON i /OFF słownika konfiguracji to za mało, bo PDFium pozwala, by jawny stan wewnątrz własnego słownika /Usage danego OCG wziął górę nad tymi tablicami, a reguła auto-stanu /AS potrafi potem nadbić obie. ISO 32000-1 §8.11.4 opisuje konfiguracje i słowniki użycia jako osobne mechanizmy; renderer PDFium skleja je w jedną decyzję, a InspectOptionalContent odtwarza ją w tej kolejności:
- Zacznij od
/BaseStatekonfiguracji, gdzie/ONi/Unchangedliczą się oba jako widoczne, a tylko/OFFukrywa - Zastosuj tablicę
/ONkonfiguracji, potem jej tablicę/OFF, więc grupa wpisana w obie kończy ukryta - Zastosuj jawny stan Usage grupy dla żądanego użycia, jak
/Usage << /View << /ViewState /OFF >> >>, który nadpisuje wszystko powyżej - Grupę, której
/Intentnie zawiera ani/View, ani/All, traktuj jako widoczną, bo nie bierze udziału w widoczności dla intencji widoku - Na koniec odpal tablicę
/ASwybranej konfiguracji, której wpisy dla pasującego zdarzenia ustawiają stan grup, które wyliczają
Trzeci krok to ten, który pali ludzi. Plik zapisany przez narzędzie do składu często niesie /ViewState /ON na każdym OCG, a PDFium ignoruje wtedy twoją starannie wyedytowaną tablicę /OFF: zapis się udaje, plik otwiera się czysto, a warstwa i tak się maluje. Dla Print i Export OcExplicitUsageState czyta najpierw PrintState albo ExportState i spada do ViewState, gdy konkretnego wpisu brak, więc samotne ViewState /ON przypina warstwę także do druku. Treść oznaczona odwołująca się do OCMD (§8.11.2.2) jest potem rozwiązywana względem tych wyników per grupa — przez politykę /P albo, gdy jest, wyrażenie widoczności /VE
Jak wypisać warstwy, które PDFium faktycznie pokaże?
TPdf.InspectOptionalContent zwraca TPdfOptionalContentInventory, którego tablica Groups niesie numer obiektu, nazwę, intencje, trzy stany Usage, język, zakres zoomu, flagę Locked, indeks grupy radiowej i wyliczone EffectiveVisible każdego OCG. Metoda każe najpierw PDFium zapisać bieżący dokument w pamięci, rozpakowuje strumienie obiektów i skanuje wynik, więc edycje zrobione wcześniej w sesji są widoczne. Indeks konfiguracji 0 to zawsze domyślny słownik /D, a wpisy /Configs następują od indeksu 1; domyślny argument -1 wybiera indeks 0. Dokument bez /OCProperties każe metodzie zwrócić False z powodem w ErrorMessage, zamiast rzucać
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
G: TPdfOptionalContentGroup;
begin
// Usage domyślnie ma ocuView; -1 wybiera konfigurację 0, słownik /D
if not Pdf.InspectOptionalContent(Inv) then
begin
Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
Exit;
end;
Memo1.Lines.Add(Format('Configuration %d: %s',
[Inv.SelectedConfigurationIndex,
string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
Tablica Memberships raportuje każdy OCMD z jego Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), surowym tekstem VisibilityExpression i własnym EffectiveVisible. Kilka reguł brzegowych jest celowych. /P ma domyślnie /AnyOn, a OCMD bez grup liczy się jako widoczny. Referencja do numeru obiektu, który nie jest znanym OCG, jest traktowana jako widoczna, zamiast zawalić całe wyrażenie. Ewaluacja /VE zatrzymuje się na głębokości zagnieżdżenia 32 i traktuje wszystko głębsze jako ukryte, więc wrogiemu albo samoreferencyjnemu wyrażeniu nie uda się zamienić inspekcji w przepełnienie stosu
Zapis nowego stanu warstw przez SaveAsOptionalContentConfigured
TPdf.SaveAsOptionalContentConfigured bierze tablicę rekordów TPdfOptionalContentStateChange (numer obiektu grupy plus Visible) i zapisuje dokument, w którym wybrana konfiguracja daje dokładnie ten stan. Wybrana konfiguracja dostaje /BaseState /ON plus kompletne tablice /ON i /OFF obejmujące każdą grupę, a każdy OCG, który już ma słownik Usage, dostaje jawny ViewState (albo PrintState / ExportState, według Options.Usage) pasujący do jego nowego stanu. Z TPdfOptionalContentConfigureOptions.Default klucz /AS wybranej konfiguracji jest usuwany, żeby zdarzenie otwarcia, druku albo eksportu nie przestawiło warstw z powrotem
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
SetLength(Changes, 2);
Changes[0].GroupObjectNumber := DimensionsObj;
Changes[0].Visible := False;
Changes[1].GroupObjectNumber := NotesObj;
Changes[1].Visible := True;
// Konfiguracja 0, ocuView, DisableAutomaticState i EnforceRadioGroups True
Options := TPdfOptionalContentConfigureOptions.Default;
if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
Changes, Options, Report) then
raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);
Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
[Report.ChangedGroupCount, Report.GroupCount,
Report.UpdatedUsageStateCount,
BoolToStr(Report.RemovedAutomaticState, True)]));
end;
Ścieżka zapisu trzyma własne wyjście zapisu PDFium jako prefiks identyczny bajt w bajt i dokleja tylko przepisanego właściciela konfiguracji oraz obiekty OCG niosące słowniki Usage, po czym idzie nowa sekcja xref i trailer. Zanim jeden bajt dotrze do twojego celu, wynik jest otwierany ponownie w osobnym TPdf pod ścisłą polityką wczytywania, a metoda zawodzi, gdy tabela odwołań krzyżowych się nie waliduje. Overload plikowy idzie o krok dalej: zapisuje do pliku tymczasowego obok celu i podmienia cel dopiero po udanej weryfikacji, więc odrzucona aktualizacja nigdy nie zostawi po sobie półnapisanego rysunku. To to samo podejście zweryfikowanej rewizji przyrostowej, którego używa edytor drzew nazw i drzew numerów w PDFium Component
Czego skonfigurowany zapis odmawia?
Skonfigurowany zapis odmawia każdej zmiany, której sam dokument zabrania albo której nie umie bezpiecznie przedstawić, i każda odmowa dzieje się, zanim cel zostanie dotknięty. Numer obiektu nieobecny w /OCGs zawodzi od razu. Zmiana grupy wpisanej w tablicę /Locked konfiguracji zawodzi, choć powtórzenie jej bieżącej wartości jest dozwolone. Z włączonym EnforceRadioGroups każdy zestaw /RBGroups, który skończyłby z więcej niż jednym widocznym członkiem, jest odrzucany, zamiast cicho wyłączyć pozostałych. Dokumenty zaszyfrowane są odrzucane, bo jawne obiekty przyrostowe nie mogą nieść aktywnego handlera bezpieczeństwa. Dokumenty podpisane rzucają EPdfError, dopóki nie podasz AllowSignedDocument = True, bo zmiana tego, co pokazuje strona, może złamać pokrycie podpisu albo politykę certyfikacji
function TFormMain.SavePrintPreset(Target: TStream;
const Changes: TPdfOptionalContentStateChanges): Boolean;
var
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
Options := TPdfOptionalContentConfigureOptions.Default;
Options.Usage := ocuPrint; // pisze /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // pierwszy wpis /Configs, nie /D
try
Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
Report); // AllowSignedDocument zostaje False
if not Result then
ShowMessage(Report.ErrorMessage);
except
on E: EPdfError do
begin
ShowMessage(E.Message); // plik podpisany: nic nie trafia do Target
Result := False;
end;
end;
end;
Zapoznaj się z trade-offami, zanim wpięjesz to w zadanie wsadowe. Doklejana rewizja siedzi na pełnym re-zapisie PDFium, nie na twoich oryginalnych bajtach pliku — dokładnie dlatego podpisane wejście potrzebuje jawnej zgody. Przepisanie normalizuje też wybraną konfigurację do /BaseState /ON, więc bazowa /Unchanged albo /OFF autora zostaje zastąpiona jawnymi tablicami o tej samej wynikowej widoczności. Zrzucenie /AS usuwa sztuczki tylko do druku, jak warstwa znaku wodnego pojawiająca się wyłącznie na papierze; ustaw DisableAutomaticState na False, żeby te reguły zostawić, akceptując, że mogą nadpisać żądany stan dla tego zdarzenia. Na plus: PDF/A-2 (ISO 19005-2, klauzula 6.9) i PDF/UA (ISO 14289-1, klauzula 7.10) oba zakazują /AS w słownikach konfiguracji, więc domyślne wyjście usuwa jeden problem, który twoja walidacja preflight PDF/A z PDFium Component inaczej by zaraportowała
Gdzie sterowanie warstwami pasuje w przeglądarce PDF w Delphi
W przeglądarce sterowanie warstwami to checklista napędzana inwentaryzacją plus ponowne wczytanie zapisanego wyniku. Wypełnij checklistę z Groups, wyłącz wpisy będące Locked, traktuj członków dzielących RadioGroupIndex jako wzajemnie wykluczających się i przy zatwierdzeniu zapisz do TMemoryStream, po czym wczytaj ten strumień z powrotem do TPdf, żeby widok namalował nowy stan. Okablowanie między TPdf a TPdfView opisuje tekst o budowie rozbudowanej przeglądarki PDF z PDFium VCL w Delphi. Licencjonowanie, wersje próbne i reszta zestawu funkcji są na stronie produktu PDFium Component dla Delphi