Technický článek

Přepínání PDF optional content vrstev v Delphi s PDFium

PDFium Component ovládá PDF optional content vrstvy (OCG) v Delphi přes dvě metody TPdf: InspectOptionalContent vypíše každou vrstvu i s viditelností, kterou PDFium doopravdy vyrenderuje, a SaveAsOptionalContentConfigured zapíše ověřenou kopii, ve které jsou vrstvy dle vaší volby zapnuté nebo vypnuté. Druhá metoda navíc neutralizuje pravidla Usage a /AS, která by jinak vaši editaci potichu zrušila. Obojí pracuje na dokumentu už otevřeném v TPdf, takže tu není druhý parser, který by se udržoval ve shodě s tím, co prohlížeč ukazuje

Požadavek typicky přijde z CAD nebo GIS dílny: výkresová sada odjíždí s rozměry, anotacemi a title blockem na samostatných vrstvách a zákazník chce kopii se skrytými rozměry, než poputuje k dodavateli. PDFium renderuje optional content správně, ale jeho veřejné ABI nemá žádnou funkci na enumeraci OCG, výběr konfigurace nebo překlopení stavu vrstvy. Takže sestoupíte na úroveň objektů, editujete /OCProperties, uložíte, načtete znovu — a vrstva je pořád tam. Příčinou je viditelnostní logika PDFium a stojí za to jí porozumět, než sahate na jakékoli byty

Proč editace /ON a /OFF nezmění, co PDFium renderuje?

Editace polí /ON a /OFF konfiguračního slovníku nestačí, protože PDFium nechá explicitní stav uvnitř vlastního slovníku /Usage OCG přebít ta pole a pravidlo auto-stavu /AS pak může přebít obojí. ISO 32000-1 §8.11.4 popisuje konfigurace a usage slovníky jako oddělené mechanismy; renderer PDFium je slepí do jediného rozhodnutí a InspectOptionalContent ho reprodukuje v tomhle pořadí:

  • Vyjděte z /BaseState konfigurace, kde /ON i /Unchanged počítají jako viditelné a jen /OFF schovává
  • Aplikujte pole /ON konfigurace, pak její pole /OFF, takže skupina uvedená v obou skončí skrytá
  • Aplikujte explicitní Usage stav skupiny pro požadované použití, jako /Usage << /View << /ViewState /OFF >> >>, který přebíjí všechno výše
  • Skupinu, jejíž /Intent neobsahuje ani /View, ani /All, berte jako viditelnou, protože se na viditelnosti podle view intentu nepodílí
  • Nakonec pusťte pole /AS vybrané konfigurace, jehož položky pro odpovídající event nastaví stav skupin, které vyjmenovávají
Pětistupňové viditelnostní rozhodnutí, které PDFium Component přehraje pro každou PDF optional content skupinu v Delphi: BaseState určí start, pole ON a OFF konfigurace se aplikují v pořadí, explicitní položka Usage ViewState nebo PrintState přebije obojí, nepodílení se podle Intent počítá jako viditelné a pole AS běží poslední
Editace polí ON a OFF nestačí, protože PDFium slepí BaseState, obě pole, Usage stav skupiny a nakonec auto-state pravidla AS do jednoho verdiktu, který InspectOptionalContent reprodukuje krok za krokem

Třetí krok je ten, který pálí. Soubor uložený layoutovým nástrojem často nese /ViewState /ON na každém OCG a PDFium pak ignoruje vaše pečlivě editované pole /OFF: uložení uspěje, soubor se čistě znovu otevře a vrstva se pořád maluje. Pro Print a Export čte OcExplicitUsageState nejdřív PrintState nebo ExportState a na ViewState spadne, když konkrétní položka chybí, takže osamocené ViewState /ON přichytí vrstvu i pro tisk. Marked content odkazující na OCMD (§8.11.2.2) se pak rozliší proti těmto per-group výsledkům, přes politiku /P nebo, je-li přítomná, viditelnostní výraz /VE

Jak vypíšete vrstvy, které PDFium doopravdy ukáže?

TPdf.InspectOptionalContent vrací TPdfOptionalContentInventory, jehož pole Groups nese objektové číslo každého OCG, název, intenty, tři Usage stavy, jazyk, zoom range, flag Locked, index radio skupiny a spočítané EffectiveVisible. Metoda nejdřív nechá PDFium uložit aktuální dokument v paměti, expanduje object streamy a proskenuje výsledek, takže se v ní odrážejí i editace provedené dřív v session. Konfigurační index 0 je vždy defaultní slovník /D a položky /Configs následují od indexu 1; defaultní argument -1 vybírá index 0. Dokument bez /OCProperties donutí metodu vrátit False s důvodem v ErrorMessage místo vyhození výjimky

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage defaultně ocuView; -1 vybere konfiguraci 0, slovník /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;

Pole Memberships hlásí každý OCMD s jeho Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), syrovým textem VisibilityExpression a vlastním EffectiveVisible. Pár hranických pravidel je záměrných. /P defaultuje na /AnyOn a OCMD bez skupin se počítá jako viditelný. Reference na objektové číslo, které není známé OCG, se bere jako viditelná místo toho, aby shodila celý výraz. Vyhodnocování /VE končí na zanoření 32 a všechno hlouběji bere jako skryté, takže nepřátelský nebo se odkazující výraz nezvrátí inspekci v stack overflow

Zápis nového stavu vrstvy přes SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured bere pole záznamů TPdfOptionalContentStateChange (objektové číslo skupiny plus Visible) a zapíše dokument, ve kterém vybraná konfigurace produkuje přesně ten stav. Vybraná konfigurace dostane /BaseState /ON plus kompletní pole /ON a /OFF pokrývající každou skupinu a každé OCG, které už má Usage slovník, dostane explicitní ViewState (nebo PrintState / ExportState dle Options.Usage) odpovídající svému novému stavu. S TPdfOptionalContentConfigureOptions.Default se klíč /AS vybrané konfigurace odstraní, takže event otevření, tisku nebo exportu nemůže vrstvy překlopit zpátky

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;

  // Konfigurace 0, ocuView, DisableAutomaticState a 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;

Zápisová cesta si drží vlastní uložený výstup PDFium jako bajt za bajtem identickou předponu a přilepí jen přepsaného vlastníka konfigurace a OCG objekty nesoucí Usage slovníky, za nimi novou xref sekci a trailer. Než jediný byte dorazí do vašeho cíle, výsledek se znovu otevře v odděleném TPdf pod přísnou load politikou a metoda selže, když tabulka cross-referencí nezvaliduje. File overload jde krok dál: píše do dočasného souboru vedle cíle a cíli nahradí až po úspěšné verifikaci, takže odmítnutá aktualizace nikdy nenechá za sebou napůl napsaný výkres. Je to tentýž ověřený přístup inkrementální revize, který používá editor PDF name tree a number tree v PDFium Component

Jak SaveAsOptionalContentConfigured v PDFium Component zapisuje Delphi PDF s přepnutými vrstvami: změny stavu a volby vlezou dovnitř, vybraná konfigurace se přepíše s kompletními poli ON a OFF a Usage stavy, přilepí se ověřená inkrementální revize a přísné znovunačtení musí zvalidovat, než se cokoli zapíše
Konfigurované uložení si drží vlastní re-save PDFium jako bajtovou předponu, přilepí přepsaného vlastníka konfigurace plus novou xref sekci a znovu otevře výsledek v odděleném TPdf, než se dotkne cíle

Co konfigurované uložení odmítá?

Konfigurované uložení odmítne každou změnu, kterou dokument sám zakazuje nebo ji nedokáže bezpečně reprezentovat, a každé odmítnutí nastane dřív, než se cíl dotkne. Objektové číslo, které není v /OCGs, selže rovnou. Změna skupiny uvedené v poli /Locked konfigurace selže, i když restatement její aktuální hodnoty je povolený. Se zapnutým EnforceRadioGroups se odmítne každá sada /RBGroups, která by skončila s víc než jedním viditelným členem, místo aby potichu vypla ostatní. Šifrované dokumenty se odmítají, protože inkrementální objekty v plaintextu nemohou nést aktivní security handler. Podepsané dokumenty vyhodí EPdfError, pokud nepředáte AllowSignedDocument = True, protože změna toho, co stránka ukazuje, může rozbít pokrytí podpisu nebo certification politiku

Brány odmítnutí, které SaveAsOptionalContentConfigured v PDFium Component aplikuje před zápisem konfigurovaného Delphi PDF: objektové číslo mimo OCGs selže, zamčené skupiny selžou, sady RBGroups s víc než jedním viditelným členem se odmítnou, šifrované dokumenty nemohou nést inkrementální objekty v plaintextu a podepsané soubory žádají AllowSignedDocument
Každé odmítnutí nastane dřív, než se cíl dotkne, a důvod selhání skončí v Report.ErrorMessage místo napůl napsaného výkresu za sebou
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // zapíše /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // první položka /Configs, ne /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument zůstává False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // podepsaný soubor: do Target se nic nezapisuje
      Result := False;
    end;
  end;
end;

Poznejte trade-offy, než tohle zapojíte do batch jobu. Přilepená revize sedí na vršku kompletního re-save PDFium, ne vašich původních bytů souboru — a přesně proto potřebuje podepsaný vstup explicitní souhlas. Přepis taky normalizuje vybranou konfiguraci na /BaseState /ON, takže autorův baseline /Unchanged nebo /OFF vystřídají explicitní pole se stejnou výslednou viditelností. Odstranění /AS zruší tiskové triky jako vrstvu s vodoznakem, která se objeví jen na papíře; nastavte DisableAutomaticState na False, abyste ta pravidla nechali, s přijetím, že pro ten event mohou přebít váš požadovaný stav. Na druhou stranu PDF/A-2 (ISO 19005-2 clause 6.9) i PDF/UA (ISO 14289-1 clause 7.10) zakazují /AS v konfiguračních slovnících, takže defaultní výstup odstraní jeden nález, který by vaše PDF/A preflight validace s PDFium Component jinak nahlásila

Kam se kontrola vrstev hodí v Delphi PDF prohlížeči

V prohlížeči je kontrola vrstev checklist řízený inventářem plus znovunačtením uloženého výsledku. Naplňte checklist z Groups, zakažte položky, které jsou Locked, berte členy sdílející RadioGroupIndex jako vzájemně se vylučující a při aplikaci zapište do TMemoryStream a ten stream načtěte zpět do TPdf, aby view namaloval nový stav. Propojení mezi TPdf a TPdfView rozebírá článek o stavbě bohatého PDF prohlížeče s PDFium VCL v Delphi. Licencování, trial download a zbytek funkcionality najdete na produktové stránce PDFium Component pro Delphi