Tehnički članak

PDF optional content slojevi u Delphi-ju uz PDFium

PDFium Component upravlja PDF optional content slojevima (OCG) u Delphi-ju kroz dve TPdf metode: InspectOptionalContent izlistava svaki sloj zajedno sa vidljivošću koju PDFium zaista renderuje, a SaveAsOptionalContentConfigured piše verifikovanu kopiju u kojoj su slojevi koje izaberete prebačeni na uključeno ili isključeno. Druga metoda takođe neutrališe Usage i /AS pravila koja bi inače tiho poništila Vašu izmenu. Obe rade na dokumentu već otvorenom u TPdf, pa nema drugog parsera koji treba držati usklađenim sa onim što viewer pokazuje

Zahtev obično stiže iz CAD ili GIS radionice: set crteža putuje sa dimenzijama, anotacijama i blokom naslova na odvojenim slojevima, a kupac želi kopiju sa sakrivenim dimenzijama pre nego što ode dobavljaču. PDFium renderuje optional content ispravno, ali njegov javni ABI nema funkciju za enumeraciju OCG-ova, izbor konfiguracije ili okretanje stanja sloja. Pa silazite na nivo objekata, izmenite /OCProperties, sačuvate, ponovo učitate, i sloj je i dalje tu. Razlog je logika vidljivosti u PDFium-u, i vredi je razumeti pre nego što dirate ijedan bajt

Zašto izmena /ON i /OFF ne menja ono što PDFium renderuje?

Izmena nizova /ON i /OFF rečnika konfiguracije nije dovoljna, jer PDFium dozvoljava da eksplicitno stanje unutar sopstvenog /Usage rečnika OCG-a pobedi te nizove, i da ih zatim oba pregazi /AS auto-state pravilo. ISO 32000-1 §8.11.4 opisuje konfiguracije i usage rečnike kao odvojene mehanizme; PDFium renderer ih skuplja u jednu odluku, i InspectOptionalContent je reprodukuje ovim redosledom:

  • Krenite od /BaseState konfiguracije, gde /ON i /Unchanged oba računaju se kao vidljivo i samo /OFF skriva
  • Primenite /ON niz konfiguracije, pa njen /OFF niz, pa grupa navedena u oba na kraju bude skrivena
  • Primenite eksplicitno Usage stanje grupe za traženu upotrebu, poput /Usage << /View << /ViewState /OFF >> >>, koje pregazi sve iznad
  • Grupu čiji /Intent ne sadrži ni /View ni /All tretirajte kao vidljivu, pošto ne učestvuje u view-intent vidljivosti
  • Napokon pokrenite /AS niz izabrane konfiguracije, čiji unosi za odgovarajući event postavljaju stanje grupa koje izlistavaju
Odluka o vidljivosti u pet koraka koju PDFium Component reprodukuje za svaku PDF optional content grupu u Delphi-ju: BaseState postavlja start, ON i OFF nizovi konfiguracije primenjuju se redom, eksplicitni Usage ViewState ili PrintState unos pregazi oba, ne-učešće u Intent računa se kao vidljivo, a AS niz radi poslednji
Izmena ON i OFF nizova nije dovoljna jer PDFium skuplja BaseState, oba niza, Usage stanje grupe i napokon AS auto-state pravila u jednu presudu koju InspectOptionalContent reprodukuje korak po korak

Treći korak je onaj koji peče ljude. Fajl sačuvan layout alatom često nosi /ViewState /ON na svakom OCG-u, i PDFium tada ignoriše Vaš pažljivo uređeni /OFF niz: čuvanje uspe, fajl se čisto otvara, i sloj se i dalje crta. Za Print i Export, OcExplicitUsageState prvo čita PrintState ili ExportState i vraća se na ViewState kad specifičnog unosa nema, pa usamljen ViewState /ON prikačuje sloj i za štampu. Marked content koji referencira OCMD (§8.11.2.2) zatim se razrešava prema ovim rezultatima po grupi, kroz /P politiku ili, kad postoji, /VE izraz vidljivosti

Kako izlistati slojeve koje će PDFium zaista pokazati?

TPdf.InspectOptionalContent vraća TPdfOptionalContentInventory čiji niz Groups nosi broj objekta svakog OCG-a, ime, intente, tri Usage stanja, jezik, zoom opseg, zastavicu Locked, indeks radio grupe i izračunatu EffectiveVisible. Metoda prvo natera PDFium da sačuva trenutni dokument u memoriji, proširi object stream-ove, i skenira rezultat, pa se izmene ranije u sesiji ogledaju. Indeks konfiguracije 0 je uvek podrazumevani rečnik /D a unosi /Configs slede od indeksa 1; podrazumevani argument -1 bira indeks 0. Dokument bez /OCProperties tera metodu da vrati False sa razlogom u ErrorMessage umesto da podigne izuzetak

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage podrazumeva ocuView; -1 bira konfiguraciju 0, rečnik /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;

Niz Memberships izveštava svaki OCMD sa njegovom Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), sirovim tekstom VisibilityExpression i sopstvenom EffectiveVisible. Par rubnih pravila je namerno. /P podrazumeva /AnyOn, i OCMD bez grupa računa se kao vidljiv. Referenca na broj objekta koji nije poznati OCG tretira se kao vidljiva umesto da obori ceo izraz. Evaluacija /VE staje na dubini ugnježđivanja 32 i sve dublje tretira kao skriveno, što sprečava neprijateljski ili samoreferencirajući izraz da pretvori inspekciju u stack overflow

Pisanje novog stanja sloja sa SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured prima niz record-a TPdfOptionalContentStateChange (broj objekta grupe plus Visible) i piše dokument u kojem izabrana konfiguracija proizvodi baš to stanje. Izabrana konfiguracija dobija /BaseState /ON plus kompletne /ON i /OFF nizove koji pokrivaju svaku grupu, i svaki OCG koji već ima Usage rečnik prima eksplicitan ViewState (ili PrintState / ExportState, po Options.Usage) koji odgovara njegovom novom stanju. Sa TPdfOptionalContentConfigureOptions.Default, ključ /AS izabrane konfiguracije se uklanja da open, print ili export event ne može da vrati slojeve nazad

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;

  // Konfiguracija 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;

Putanja upisa zadržava sopstveni sačuvani izlaz PDFium-a kao prefiks bajt po bajt i dodaje samo preslovljenog vlasnika konfiguracije i OCG objekte koji nose Usage rečnike, iza kojih ide novi xref odeljak i trailer. Pre nego što ijedan bajt stigne do odredišta, rezultat se ponovo otvara u posebnom TPdf-u pod strogom politikom učitavanja, i metoda pada ako tabela unakrsnih referenci ne validira. File overload ide korak dalje: piše u privremeni fajl pored cilja i zamenjuje cilj tek kad verifikacija uspe, pa odbijena izmena nikada ne ostavlja upola napisan crtež. To je isti pristup verifikovane inkrementalne revizije koji koristi PDF name tree i number tree editor u PDFium Component-u

Kako SaveAsOptionalContentConfigured u PDFium Component-u piše Delphi PDF sa prebačenim slojevima: izmene stanja i opcije ulaze, izabrana konfiguracija se preslovljava sa kompletnim ON i OFF nizovima i Usage stanjima, verifikovana inkrementalna revizija se dodaje, i strogo ponovno otvaranje mora validirati pre nego što bilo šta se upiše
Konfigurisano čuvanje zadržava sopstveni re-save PDFium-a kao bajt prefiks, dodaje preslovljenog vlasnika konfiguracije plus novi xref odeljak, i ponovo otvara rezultat u posebnom TPdf-u pre nego što se odredište dirne

Šta konfigurisano čuvanje odbija da učini?

Konfigurisano čuvanje odbija svaku izmenu koju sam dokument zabranjuje ili ne može bezbedno da predstavi, i svako odbijanje dešava se pre nego što se odredište dirne. Broj objekta koji nije u /OCGs pada odmah. Izmena grupe navedene u /Locked nizu konfiguracije pada, mada je ponavljanje njene trenutne vrednosti dozvoljeno. Sa uključenim EnforceRadioGroups, svaki /RBGroups skup koji bi na kraju imao više od jednog vidljivog člana odbija se umesto da se ostali tiho isključe. Šifrovani dokumenti se odbijaju jer otvoreni tekst inkrementalnih objekata ne može da nosi aktivni security handler. Potpisani dokumenti podižu EPdfError osim ako prosledite AllowSignedDocument = True, pošto izmena onoga što stranica pokazuje može da slomi pokriće potpisa ili certification politiku

Kapije odbijanja koje SaveAsOptionalContentConfigured primenjuje u PDFium Component-u pre pisanja konfigurisanog Delphi PDF-a: broj objekta van OCG-ova pada, zaključane grupe padaju, RBGroups skupovi sa više od jednog vidljivog člana se odbijaju, šifrovani dokumenti ne mogu da nose inkrementalne objekte otvorenog teksta, a potpisani fajlovi traže AllowSignedDocument
Svako odbijanje dešava se pre nego što se odredište dirne, i razlog neuspeha sleti u Report.ErrorMessage umesto da ostavi upola napisan crtež
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;    // prvi unos /Configs, ne /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument ostaje False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // potpisan fajl: ništa nije upisano u Target
      Result := False;
    end;
  end;
end;

Znajte kompromise pre nego što ovo uvežete u batch posao. Dodata revizija sedi na vrhu punog re-save-a PDFium-a, ne na Vašim originalnim bajtovima fajla, i baš zato potpisani ulaz traži eksplicitnu saglasnost. Preslovljavanje takođe normalizuje izabranu konfiguraciju na /BaseState /ON, pa se autorov /Unchanged ili /OFF baseline zamenjuje eksplicitnim nizovima sa istom rezultujućom vidljivošću. Ispuštanje /AS skida trikove samo za štampu poput sloja vodenog žiga koji se pojavi samo na papiru; postavite DisableAutomaticState na False da zadržite ta pravila, prihvativši da mogu da pregaze Vaše traženo stanje za taj event. S pozitivne strane, PDF/A-2 (ISO 19005-2 klauza 6.9) i PDF/UA (ISO 14289-1 klauza 7.10) oba zabranjuju /AS u rečnicima konfiguracije, pa podrazumevani izlaz skida jedan problem koji bi Vaša PDF/A preflight validacija sa PDFium Component-om inače prijavila

Gde kontrola slojeva staje u Delphi PDF viewer

U viewer-u, kontrola slojeva je checklist-a koja se pokreće inventarom plus ponovnim učitavanjem sačuvanog rezultata. Napunite checklist-u iz Groups, onemogućite unose koji su Locked, tretirajte članove koji dele RadioGroupIndex kao međusobno isključive, i pri primeni upišite u TMemoryStream i učitajte taj stream nazad u TPdf da view nacrta novo stanje. Ožičenje između TPdf-a i TPdfView-a pokriveno je u gradnji PDF viewer-a bogatog fičerima sa PDFium VCL u Delphi-ju. Licenciranje, trial preuzimanja i ostatak seta fičera su na stranici proizvoda PDFium Component za Delphi