Artykuł techniczny

Przełączanie warstw opcjonalnej treści PDF w Delphi z PDFium

Zestaw rysunków przychodzi z wymiarami na osobnej warstwie, a oczywisty sposób ich ukrycia to przeniesienie odwołania tej grupy z tablicy /ON domyślnej konfiguracji do /OFF. Plik zapisuje się i otwiera ponownie, a linie wymiarowe nadal są drukowane przez tabliczkę rysunku. Przyczyna siedzi o obiekt dalej, wewnątrz słownika grupy: /Usage << /View << /ViewState /ON >> >> — stan jawny, który renderer czyta przed tablicami konfiguracji

Stan warstwy może mieszkać w kilku miejscach PDF-a, a edycja /OCProperties dotyka tylko jednego z nich. Silnik PDFium od Google renderuje zawartość opcjonalną poprawnie, ale jego publiczne API C nie ma funkcji do enumerowania grup zawartości opcjonalnej (OCG), wybierania konfiguracji albo włączania i wyłączania grupy. Kod Delphi sterujący warstwami musi sam przepisać obiekty i odtworzyć dokładnie kolejność decyzji renderera, albo jego lista warstw raportuje jeden stan, a strona pokazuje inny

Dlaczego edycja /ON i /OFF nie zmienia tego, co renderuje PDFium?

Tablice /ON i /OFF to dopiero drugi z pięciu kroków, a dwa późniejsze kroki potrafią je obalić. ISO 32000-1 §8.11.4.3 definiuje słownik konfiguracji zawartości opcjonalnej z /BaseState, /ON, /OFF, /Locked, /RBGroups i /AS, podczas gdy §8.11.4.4 definiuje słownik Usage, który OCG może nieść. W specyfikacji Usage opisuje, do czego grupa służy, a wpisy auto-stanu w /AS zamieniają ten opis w ON albo OFF. PDFium idzie krótszą drogą: gdy renderuje w jakimś celu, własny jawny stan grupy dla tego celu wygrywa. Złożone razem, kolejność wygląda tak:

  • Startuj od /BaseState konfiguracji, gdzie /ON i /Unchanged liczą się oba jako widoczne, a tylko /OFF ukrywa
  • Zastosuj tablicę /ON, potem tablicę /OFF, więc grupa wymieniona w obu ląduje ukryta
  • Zastosuj jawny stan Usage grupy dla żądanego celu, jak /Usage << /View << /ViewState /OFF >> >>, który nadpisuje wszystko powyżej
  • Grupę, której /Intent nie zawiera ani /View, ani /All, traktuj jako widoczną, bo nie bierze udziału w widoczności z intencją widoku
  • Tablicę /AS wybranej konfiguracji puść ostatnią, pozwalając wpisom pasującego zdarzenia ustawić stan grup, które wymieniają
Pięciokrokowa decyzja o widoczności, którą PDFium Component odtwarza dla każdej grupy zawartości opcjonalnej PDF w Delphi: BaseState ustawia start, tablice konfiguracji ON i OFF nakładają się w kolejności, jawny wpis Usage ViewState albo PrintState nadpisuje oba, brak udziału Intent liczy się jako widoczny, a tablica AS biegnie ostatnia
Edycja tablic ON i OFF nie wystarcza, bo PDFium składa BaseState, obie tablice, stan Usage grupy i na końcu reguły auto-stanu AS w jeden werdykt, który InspectOptionalContent odtwarza krok po kroku

Krok trzeci wyjaśnia plik z otwarcia: niektóre exporterzy zapisują /ViewState /ON w każdej tworzonej grupie i od tej chwili żadna ilość edycji tablic nie ukryje tych warstw. Druk i eksport podążają tą samą regułą z jednym skrętem: renderer czyta najpierw /PrintState albo /ExportState i spada na /ViewState, gdy konkretnego wpisu brak. Samotne /ViewState /ON przypina warstwę więc i na papierze. Krok czwarty jest ciszej. Grupa otagowana /Intent /Design jest całkiem poza widocznością z intencją widoku i maluje się niezależnie od tablic, podczas gdy grupa bez klucza /Intent jest traktowana jako /View — domyślny, który daje specyfikacja

Wylistowanie warstw i ich stanu renderowania przez InspectOptionalContent

PDFium Component odtwarza tę kolejność w TPdf.InspectOptionalContent. Metoda wypełnia TPdfOptionalContentInventory i zwraca True przy sukcesie; parametr Usage ma domyślnie ocuView, a parametr ConfigurationIndex domyślnie -1, co wybiera indeks 0. Indeks konfiguracji 0 to domyślny słownik /D, a wpisy /Configs ciągną się od indeksu 1 w kolejności plikowej. Przed skanowaniem metoda każe PDFium zapisać otwarty dokument do pamięci i rozwija strumienie obiektów, więc edycje zrobione wcześniej w sesji pokazują się w wyniku. Plik bez /OCProperties albo indeks konfiguracji poza dostępnym zakresem każe wywołaniu zwrócić False z powodem w ErrorMessage; wyjątek nie jest podnoszony w żadnym z tych przypadków

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  Cfg: TPdfOptionalContentConfiguration;
  G: TPdfOptionalContentGroup;
begin
  // Usage domyślnie 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;
  Cfg := Inv.Configurations[Inv.SelectedConfigurationIndex];
  Memo1.Lines.Add(Format('Configuration %d "%s", %d groups, /AS present: %s',
    [Inv.SelectedConfigurationIndex, string(Cfg.Name), Length(Inv.Groups),
     BoolToStr(Cfg.HasAutomaticState, True)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %-24s visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

Każde TPdfOptionalContentGroup niesie numer obiektu, nazwę, Intents i trzy jawne stany ViewState, PrintState i ExportState, każdy z ocsUnspecified, ocsOn albo ocsOff. Gdy stan czyta ocsOn, a grupa siedzi w tablicy /OFF, znalazłeś pułapkę z akapitu otwierającego. Locked i RadioGroupIndex pochodzą z wybranej konfiguracji, a RadioGroupIndex wynosi -1 dla grupy, która nie należy do żadnego zestawu /RBGroups

Reszta słownika Usage jest raportowana tak, jak stanowi plik: Language i LanguagePreferred, UserType i UserNames, PageElementSubtype oraz MinimumZoom i MaximumZoom za flagą HasZoomRange. EffectiveVisible nie zależy od żadnego z nich. Wyświetlanie zależne od powiększenia to sprawa twojej przeglądarki. Każde TPdfOptionalContentConfiguration wystawia nazwę, BaseState, zawartość tablic i RadioGroups, plus OrderGroupObjectNumbers, które spłaszcza /Order w odwołania do grup i pomija etykiety tekstowe zagnieżdżonych tablic etykiet

Druk i eksport mogą się nie zgadzać z ekranem

Argument Usage zmienia odpowiedź i po to on istnieje. Pod ocuPrint inwentarz czyta /PrintState z jego fallbackiem na /ViewState, a udział biorą wyłącznie wpisy /AS, których /Event to /Print. Warstwa znaku wodnego zbudowana do pojawiania się tylko na papierze raportuje więc ukrytą pod ocuView i widoczną pod ocuPrint. Porównanie obu przebiegów znajduje takie warstwy, zanim wydruk kogokolwiek zaskoczy

procedure TFormMain.ReportPrintOnlyLayers;
var
  OnScreen, OnPaper: TPdfOptionalContentInventory;
  I: Integer;
begin
  if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
     not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
    Exit;
  // Oba przebiegi czytają tę samą tablicę /OCGs, więc kolejność grup pasuje
  for I := 0 to High(OnScreen.Groups) do
    if OnScreen.Groups[I].EffectiveVisible <> OnPaper.Groups[I].EffectiveVisible then
      Memo1.Lines.Add(Format('%s: screen=%s, print=%s',
        [string(OnScreen.Groups[I].Name),
         BoolToStr(OnScreen.Groups[I].EffectiveVisible, True),
         BoolToStr(OnPaper.Groups[I].EffectiveVisible, True)]));
end;

Jak są oceniane wyrażenia widoczności OCMD?

Słownik członkostwa zawartości opcjonalnej (OCMD, ISO 32000-1 §8.11.2.2) wiąże fragment treści z kilkoma grupami przez politykę /P albo przez wyrażenie widoczności /VE, łączące grupy przez /And, /Or i /Not. Specyfikacja daje /P domyślny /AnyOn, pozwala /VE mieć pierwszeństwo przed /OCGs i /P, gdy jest obecne, i mówi, że OCMD bez grup nie ma wpływu na widoczność

Tablica Memberships inwentarza podąża za tymi regułami w oparciu o wyniki per grupa z tego samego przebiegu, więc usage, o który prosiłeś, przechodzi dalej. Każde członkostwo raportuje numery obiektów swoich grup, Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff albo ocmpUnknown dla nazwy spoza tej czwórki), surowy tekst VisibilityExpression i własne EffectiveVisible. OCMD bez grup liczy się jako widoczny. Odwołanie do numeru obiektu, który nie jest znanym OCG, też liczy się jako widoczne, więc jedno wiszące odwołanie nie ukrywa wszystkiego wokół. Ocena /VE zatrzymuje się na głębokości zagnieżdżenia 32 i traktuje wszystko głębsze jako ukryte, więc samoodwołujące się wyrażenie nie przepełni stosu. Skan wylistowuje każdy OCMD składowany jako obiekt pośredni z /Type /OCMD, strumienie obiektów włącznie

Zapisywanie nowego stanu warstw przez SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured bierze tablicę rekordów TPdfOptionalContentStateChange, z których każdy trzyma GroupObjectNumber i flagę Visible. Zapisuje kopię, w której wybrana konfiguracja produkuje dokładnie ten stan pod pięciokrokową kolejnością. Grupy, których nie wspomnisz, zachowują widoczność, którą policzył dla nich inwentarz

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer;
  ForPrint: Boolean);
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;

  Options := TPdfOptionalContentConfigureOptions.Default;
  if ForPrint then
  begin
    Options.Usage := ocuPrint;         // zapisuje /Print << /PrintState ... >>
    Options.ConfigurationIndex := 1;   // pierwszy wpis /Configs, /D zostaje takie, jakie było
  end;

  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;

Wyjście zachowuje własny zapis dokumentu PDFium jako przedrostek bajt-w-bajt i dokłada wyłącznie przepisanego właściciela konfiguracji i obiekty OCG niosące słowniki Usage, po czym nową sekcję xref i trailer. Zanim cokolwiek dotrze do twojego celu, wynik jest otwierany ponownie w osobnym TPdf pod ścisłą polityką wczytywania, a metoda zawodzi, gdy to wczytanie się nie powiedzie albo tablica odwołań krzyżowych nie przejdzie walidacji. Przeładowanie plikowe zapisuje do pliku tymczasowego obok celu i podmienia cel dopiero po weryfikacji, więc odrzucona aktualizacja zostawia poprzedni rysunek nietknięty. To to samo podejście zweryfikowanej rewizji przyrostowej, którego używa edytor drzewa nazw i drzewa numerów PDF w PDFium Component. Żeby potwierdzić preset druku, wczytaj wyjście ponownie i wywołaj InspectOptionalContent(Inv, ocuPrint, 1); grupy powinny raportować stany, które podałeś

Jak SaveAsOptionalContentConfigured w PDFium Component zapisuje PDF Delphi z przełączonymi warstwami: zmiany stanów i opcje wchodzą, wybrana konfiguracja jest przepisywana z kompletnymi tablicami ON i OFF oraz stanami Usage, zweryfikowana rewizja przyrostowa jest dokładana, a ścisłe ponowne otwarcie musi przejść walidację, zanim cokolwiek zostanie zapisane
Skonfigurowany zapis zachowuje własny ponowny zapis PDFium jako przedrostek bajtów, dokłada przepisanego właściciela konfiguracji plus nową sekcję xref i otwiera wynik ponownie w osobnym TPdf, zanim cel zostanie tknięty

Czego skonfigurowany zapis odmawia?

Skonfigurowany zapis odmawia każdej zmiany, której dokument zakazuje albo nie umie bezpiecznie unieść, i każda odmowa dzieje się, zanim cel zostanie zapisany. Kontrole biegną w stałej kolejności. Podpisany dokument podnosi EPdfError, zanim cokolwiek innego się wydarzy, chyba że podasz AllowSignedDocument = True. Dokument zaszyfrowany jest odrzucany następny, bo obiekty przyrostowe w jawnym tekście nie mogą unieść aktywnego handlera bezpieczeństwa. Potem sprawdzana jest każda żądana zmiana: numer obiektu, którego nie ma w /OCGs, oblega od razu, a zmiana grupy wymienionej w tablicy /Locked konfiguracji oblega, choć powtórzenie jej bieżącej wartości przechodzi. Na końcu, przy włączonym EnforceRadioGroups, zestaw /RBGroups, który skończyłby z więcej niż jednym widocznym członkiem, jest odrzucany. Każda odmowa poza tą od podpisu zwraca False z powodem w Report.ErrorMessage

Bramki odmowy, które SaveAsOptionalContentConfigured stosuje w PDFium Component przed zapisem skonfigurowanego PDF Delphi: numer obiektu poza OCGs oblega, grupy zablokowane oblegają, zestawy RBGroups z więcej niż jednym widocznym członkiem są odrzucane, dokumenty zaszyfrowane nie uniosą obiektów przyrostowych w jawnym tekście, a pliki podpisane żądają AllowSignedDocument
Każda odmowa dzieje się, zanim cel zostanie tknięty; powód ląduje w Report.ErrorMessage, albo w komunikacie EPdfError, gdy plik jest podpisany

Dwie z tych bramek idą dalej, niż wymagaje to specyfikacja w ścisłym sensie. §8.11.4.3 opisuje /Locked jako ochronę przed zmianami przez interfejs użytkownika przeglądarki i opisuje grupy przycisków radiowych jako wyłączające pozostałych członków, gdy jeden zostaje włączony. PDFium Component stosuje obie także do zmian programowych, a przy grupach radiowych odmawia zamiast wybierać członków do wyłączenia. Poproszony o dwóch członków wzajemnie wykluczającego się zestawu, biblioteka nie może wiedzieć, o którego ci chodziło, a zgadywanie przeczy twojemu żądaniu. Wyłącz pozostałych w tej samej tablicy Changes, a zapis przejdzie

Bramka dokumentu podpisanego istnieje, bo dokładana rewizja siedzi na wierzchu pełnego ponownego zapisu PDFium, który jest nową sekwencją bajtów; zakres bajtów, który pokrywa istniejący podpis, nie może być uznany za ocalały. Podawaj AllowSignedDocument = True tylko wtedy, gdy utrata tego podpisu jest do przyjęcia

Trzy skutki uboczne mają znaczenie w zadaniu wsadowym. Przepisanie normalizuje wybraną konfigurację do /BaseState /ON, więc baseline autora /Unchanged albo /OFF staje się jawnymi tablicami z tą samą wynikową widocznością. Zrzucenie /AS usuwa też drukowe sztuczki, jak warstwa znaku wodnego pojawiająca się tylko na papierze; ustaw DisableAutomaticState na False, żeby zachować te reguły i zaakceptować, że mogą nadpisać żądany stan dla swojego zdarzenia. A tablice /ON i /OFF są zapisywane ze stanów policzonych dla Options.Usage, więc preset druku zapisany do konfiguracji 0 zmienia też to, co pokazuje ekran, dla każdej grupy bez słownika Usage. Trzymaj presety druku we wpisie /Configs i zostaw /D ekranowi. Po stronie zgodności 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 czyści jeden finding, który twoja walidacja preflight PDF/A z PDFium Component inaczej by raportowała

Nakładanie zmian warstw wewnątrz przeglądarki Delphi

W przeglądarce kontrola warstw to checklist zbudowany z inwentarza, po czym zapis do pamięci i ponowne wczytanie. Wypełnij checklist z Groups, wyłącz wpisy z ustawioną flagą Locked, traktuj członków dzielących RadioGroupIndex jako wzajemnie wykluczających się, a przy zatwierdzaniu zapisz do TMemoryStream przez przeładowanie strumieniowe. TPdf.LoadDocument oczekuje nieaktywnego komponentu, więc najpierw go wyłącz; buforowane wczytanie kopiuje bajty, co pozwala zwolnić strumień od razu

procedure TFormMain.ApplyLayerChecklist(
  const Changes: TPdfOptionalContentStateChanges);
var
  Report: TPdfOptionalContentConfigureReport;
  Output: TMemoryStream;
  KeepPage: Integer;
begin
  Output := TMemoryStream.Create;
  try
    try
      if not Pdf.SaveAsOptionalContentConfigured(Output, Changes,
        TPdfOptionalContentConfigureOptions.Default, Report) then
      begin
        ShowMessage('Layer update rejected: ' + Report.ErrorMessage);
        Exit;
      end;
    except
      on E: EPdfError do
      begin
        ShowMessage(E.Message);       // podpisany plik: Output wciąż jest pusty
        Exit;
      end;
    end;
    KeepPage := Pdf.PageNumber;
    Pdf.Active := False;
    Pdf.LoadDocument(Output);         // Buffered domyślnie True
    Pdf.PageNumber := KeepPage;
  finally
    Output.Free;
  end;
end;

Po ponownym wczytaniu PDFium maluje z nowej konfiguracji, więc ekran i następne wywołanie InspectOptionalContent się zgadzają. Okablowanie między TPdf i TPdfView omawia budowa bogatego w funkcje przeglądarki PDF z PDFium VCL w Delphi. Gdy dokument nie ma jeszcze warstw, a potrzebujesz je dodać, tworzenie warstw PDF z OCG i OCMD w Delphi przechodzi przez SaveAsOptionalContentCreated

Cennik, build próbny i pozostałe funkcje dokumentowe komponentu są wypisane na stronie produktu PDFium Component dla Delphi