Odborný článok

Prepínanie PDF optional content vrstiev v Delphi s PDFium

PDFium Component ovláda PDF optional content vrstvy (OCG) v Delphi cez dve metódy TPdf: InspectOptionalContent vypíše každú vrstvu spolu s viditeľnosťou, ktorú PDFium reálne vyrenderuje, a SaveAsOptionalContentConfigured zapíše overenú kópiu, v ktorej sú zvolené vrstvy zapnuté alebo vypnuté. Druhá metóda zároveň neutralizuje Usage a /AS pravidlá, ktoré by inak poticho vrátili vašu úpravu späť. Obe pracujú na dokumente už otvorenom v TPdf, takže niet druhého parsera, ktorý by trebalo držať v synchronizácii s tým, čo viewer ukazuje

Požiadavka obyčajne prichádza z CAD či GIS dielne: výkresová sada odchádza s rozmermi, anotáciami a title blockom na samostatných vrstvách a zákazník chce kópiu so skrytými rozmermi skôr, než putuje k dodávateľovi. PDFium renderuje optional content správne, ale jeho verejné ABI nemá žiadnu funkciu na enumeráciu OCG, výber konfigurácie alebo preklopenie stavu vrstvy. Takže zostúpite na úroveň objektov, upravíte /OCProperties, uložíte, znova načítate a vrstva je stále tam. Dôvodom je viditeľnostná logika PDFium a stojí za pochopenie skôr, než sa siahne na akýkoľvek bajt

Prečo úprava /ON a /OFF nemení to, čo PDFium renderuje?

Úprava polí /ON a /OFF konfiguračného slovníka nestačí, pretože PDFium nechá explicitný stav vo vnútri vlastného slovníka /Usage OCG zvýaziť nad tými poľami a pravidlo auto-stavu /AS potom môže preklopiť oboje. ISO 32000-1 §8.11.4 popisuje konfigurácie a usage slovníky ako oddelené mechanizmy; renderer PDFium ich zlepí do jedného rozhodnutia a InspectOptionalContent ho reprodukuje v tomto poradí:

  • Začnite od /BaseState konfigurácie, kde /ON aj /Unchanged počítajú ako viditeľné a len /OFF skrýva
  • Aplikujte pole /ON konfigurácie, potom jej pole /OFF, takže skupina uvedená v oboch skončí skrytá
  • Aplikujte explicitný Usage stav skupiny pre požadované použitie, ako /Usage << /View << /ViewState /OFF >> >>, ktorý preklopí všetko vyššie
  • Berte skupinu, ktorej /Intent neobsahuje ani /View, ani /All, ako viditeľnú, keďže sa nezúčastňuje view-intent viditeľnosti
  • Nakoniec pusťte pole /AS zvolenej konfigurácie, ktorého záznamy pre sediacu udalosť nastavia stav skupín, ktoré vypisujú
Pätikrokové viditeľnostné rozhodnutie, ktoré PDFium Component prehráva pre každú PDF optional content skupinu v Delphi: BaseState nastaví štart, polia ON a OFF konfigurácie sa aplikujú v poradí, explicitný Usage ViewState alebo PrintState záznam preklopí oboje, neúčasť Intent počíta ako viditeľné a pole AS beží posledné
Úprava polí ON a OFF nestačí, pretože PDFium zlepí BaseState, obe polia, Usage stav skupiny a nakoniec pravidlá auto-stavu AS do jedného verdiktu, ktorý InspectOptionalContent reprodukuje krok za krokom

Tretí krok je ten, ktorý páli ľudí. Súbor uložený layoutovým nástrojom často nesie /ViewState /ON na každom OCG a PDFium potom ignoruje vaše starostlivo upravené pole /OFF: uloženie sa podarí, súbor sa čisto znova otvorí a vrstva sa stále kreslí. Pre Print a Export číta OcExplicitUsageState najprv PrintState alebo ExportState a padne späť na ViewState, keď konkrétny záznam chýba, takže osamelé ViewState /ON pripína vrstvu aj pre tlač. Marked content odkazujúci na OCMD (§8.11.2.2) sa potom vyhodnocuje proti týmto výsledkom na skupinu, cez politiku /P alebo, keď je prítomná, cez viditeľnostný výraz /VE

Ako vypíšete vrstvy, ktoré PDFium reálne ukáže?

TPdf.InspectOptionalContent vracia TPdfOptionalContentInventory, ktorého pole Groups nesie objektové číslo každého OCG, meno, intenty, tri Usage stavy, jazyk, zoom range, flag Locked, radio-group index a vypočítané EffectiveVisible. Metóda najprv nechá PDFium uložiť aktuálny dokument v pamäti, expanduje object streams a preskánuje výsledok, takže sa odzrkadlia úpravy urobené skôr v session. Konfiguračný index 0 je vždy default slovník /D a záznamy /Configs nasledujú od indexu 1; default argument -1 vyberá index 0. Dokument bez /OCProperties donúti metódu vrátiť False s dôvodom v ErrorMessage namiesto výnimky

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage má default ocuView; -1 vyberá konfiguráciu 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ási každý OCMD s jeho Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), surovým textom VisibilityExpression a jeho vlastným EffectiveVisible. Pár hraničných pravidiel je zámerných. /P má default /AnyOn a OCMD bez skupín počíta ako viditeľný. Odkaz na objektové číslo, ktoré nie je známe OCG, sa berie ako viditeľný namiesto toho, aby zlyhal celý výraz. Vyhodnocovanie /VE zastaví na vnorení hĺbky 32 a čokoľvek hlbšie berie ako skryté, čo bráni nepriateľskému alebo seba-odkazujúcemu výrazu zmeniť inšpekciu na stack overflow

Zápis nového stavu vrstvy cez SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured berie pole recordov TPdfOptionalContentStateChange (objektové číslo skupiny plus Visible) a zapisuje dokument, v ktorom zvolená konfigurácia produkuje presne ten stav. Zvolená konfigurácia dostane /BaseState /ON plus kompletné polia /ON a /OFF pokrývajúce každú skupinu a každý OCG, ktorý už má Usage slovník, dostane explicitný ViewState (alebo PrintState / ExportState, podľa Options.Usage) sediaci s jeho novým stavom. S TPdfOptionalContentConfigureOptions.Default sa kľúč /AS zvolenej konfigurácie odstráni, takže udalosť open, print alebo export nedokáže vrstvy preklopiť späť

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;

  // Konfigurácia 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 drží vlastný uložený výstup PDFium ako bajt-za-bajtom prefix a pripája len prepísaného vlastníka konfigurácie a OCG objekty nesúce Usage slovníky, za nimi novú xref sekciu a trailer. Skôr než jediný bajt dosiahne na váš cieľ, výsledok sa znova otvorí v samostatnom TPdf pod prísnou load politikou a metóda zlyhá, keď cross-reference tabuľka nevaliduje. File overload ide ešte ďalej: zapisuje do dočasného súboru vedľa cieľa a nahradí cieľ až po úspešnej verifikácii, takže odmietnutá aktualizácia nikdy nezanechá polozapísaný výkres. Je to ten istý prístup overenej inkrementálnej revízie, aký používa editor PDF name tree a number tree v PDFium Component

Ako zapisuje SaveAsOptionalContentConfigured v PDFium Component Delphi PDF s prepnutými vrstvami: zmeny stavu a options vletia dnu, zvolená konfigurácia sa prepíše s kompletnými poľami ON a OFF a Usage stavmi, overená inkrementálna revízia sa pripojí a prísne znovuotvorenie musí validovať skôr, než sa čokoľvek zapíše
Konfigurované ukladanie drží vlastný re-save PDFium ako bajtový prefix, pripája prepísaného vlastníka konfigurácie plus novú xref sekciu a znova otvára výsledok v samostatnom TPdf skôr, než sa siahne na cieľ

Čo odmieta konfigurované ukladanie robiť?

Konfigurované ukladanie odmietne každú zmenu, ktorú dokument sám zakazuje alebo nedokáže bezpečne reprezentovať, a každé odmietnutie sa stane skôr, než sa siahne na cieľ. Objektové číslo, ktoré nie je v /OCGs, zlyhá rovno. Zmena skupiny uvedenej v poli /Locked konfigurácie zlyhá, hoci zopakovanie jej aktuálnej hodnoty je povolené. So zapnutým EnforceRadioGroups sa odmietne každá sada /RBGroups, ktorá by skončila s viac než jedným viditeľným členom, namiesto tichého vypnutia ostatných. Šifrované dokumenty sa odmietajú, pretože plaintextové inkrementálne objekty nedokážu niesť aktívny security handler. Podpísané dokumenty vyhodia EPdfError, pokiaľ nepošlete AllowSignedDocument = True, keďže zmena toho, čo stránka ukazuje, dokáže zlomiť pokrytie podpisu alebo certification politiku

Brány odmietnutia, ktoré aplikuje SaveAsOptionalContentConfigured v PDFium Component pred zápisom konfigurovaného Delphi PDF: objektové číslo mimo OCG zlyhá, zamknuté skupiny zlyhajú, sady RBGroups s viac než jedným viditeľným členom sa odmietnu, šifrované dokumenty nedokážu niesť plaintextové inkrementálne objekty a podpísané súbory vyžadujú AllowSignedDocument
Každé odmietnutie sa stane skôr, než sa siahne na cieľ, a dôvod zlyhania pristane v Report.ErrorMessage namiesto zanechania polozapísaného výkresu
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // zapisuje /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // prvý záznam /Configs, nie /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument zostáva False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // podpísaný súbor: do Target sa nič nezapíše
      Result := False;
    end;
  end;
end;

Poznajte trade-offy skôr, než toto zapojíte do batch jobu. Pripojená revízia sedí na vrchu úplného re-save PDFium, nie pôvodných bajtov vášho súboru, a presne preto podpísaný vstup potrebuje explicitný súhlas. Prepis tiež normalizuje zvolenú konfiguráciu na /BaseState /ON, takže baseline /Unchanged alebo /OFF od autora sa nahradí explicitnými poľami s rovnakou výslednou viditeľnosťou. Zahodenie /AS odstráni print-only triky ako watermark vrstvu, ktorá sa ukáže len na papieri; nastavte DisableAutomaticState na False, aby tie pravidlá zostali, s tým, že môžu preklopiť váš požadovaný stav pre tú udalosť. Na druhej strane PDF/A-2 (ISO 19005-2 clause 6.9) aj PDF/UA (ISO 14289-1 clause 7.10) zakazujú /AS v konfiguračných slovníkoch, takže default výstup odstráni jeden problém, ktorý by inak hlásila PDF/A preflight validácia s PDFium Component

Kde sa ovládanie vrstiev hodí do Delphi PDF vieweru

Vo vieweri je ovládanie vrstiev checklist hnaný inventárom plus reloadom uloženého výsledku. Naplňte checklist z Groups, zneprístupnite záznamy, ktoré sú Locked, berte členov zdieľajúcich RadioGroupIndex ako vzájomne sa vylučujúcich a pri aplikovaní zapisujte do TMemoryStream a načítajte ten stream späť do TPdf, aby view nakreslil nový stav. Prepojenie medzi TPdf a TPdfView pokrýva stavba bohatého PDF vieweru s PDFium VCL v Delphi. Licencovanie, trial sťahovania a zvyšok funkcionality sú na produktovej stránke PDFium Component for Delphi