Articol tehnic

Comută straturile PDF de conținut opțional în Delphi

PDFium Component controlează straturile de conținut opțional PDF (OCG) în Delphi prin două metode TPdf: InspectOptionalContent listează fiecare strat împreună cu vizibilitatea pe care PDFium o va randa efectiv, iar SaveAsOptionalContentConfigured scrie o copie verificată în care straturile alese de dvs. sunt pornite sau oprite. A doua metodă neutralizează de asemenea regulile Usage și /AS care altfel ar desface în tăcere editarea dvs. Ambele lucrează pe documentul deja deschis în TPdf, deci nu există un al doilea parser de ținut în sincron cu ce arată viewer-ul

Cererea vine de obicei dintr-un atelier de CAD sau GIS: setul de desene se livrează cu dimensiuni, adnotări și un bloc de titlu pe straturi separate, iar clientul vrea o copie cu dimensiunile ascunse înainte să ajungă la un furnizor. PDFium randează corect conținutul opțional, dar ABI-ul lui public nu are nicio funcție de enumerare a OCG-urilor, de alegere a unei configurații sau de răsturnare a stării unui strat. Deci coborâți la nivel de obiect, editați /OCProperties, salvați, reîncărcați, iar stratul este tot acolo. Motivul este logica de vizibilitate a PDFium și merită înțeleasă înainte să atingeți vreun byte

De ce nu schimbă editarea /ON și /OFF ce randează PDFium?

Editarea tablourilor /ON și /OFF ale dicționarului de configurație nu este de ajuns, pentru că PDFium lasă o stare explicită din propriul dicționar /Usage al OCG-ului să învingă acele tablouri, iar o regulă auto-state /AS le poate suprascrie apoi pe ambele. ISO 32000-1 §8.11.4 descrie configurațiile și dicționarele de usage ca mecanisme separate; renderer-ul PDFium le împăturește într-o singură decizie, iar InspectOptionalContent o reproduce în ordinea aceasta:

  • Plecați de la /BaseState al configurației, unde /ON și /Unchanged contează ambele ca vizibile și doar /OFF ascunde
  • Aplicați tabloul /ON al configurației, apoi tabloul lui /OFF, astfel încât un grup listat în ambele ajunge ascuns
  • Aplicați starea Usage explicită a grupului pentru usage-ul cerut, precum /Usage << /View << /ViewState /OFF >> >>, care suprascrie tot ce este mai sus
  • Tratați ca vizibil un grup al cărui /Intent nu conține nici /View, nici /All, fiindcă nu ia parte la vizibilitatea cu intent de vizualizare
  • În final rulați tabloul /AS al configurației selectate, ale cărui intrări pentru evenimentul potrivit setează starea grupurilor pe care le listează
Decizia de vizibilitate în cinci pași pe care PDFium Component o redă pentru fiecare grup de conținut opțional PDF în Delphi: BaseState stabilește pornirea, tablourile ON și OFF ale configurației se aplică în ordine, o intrare Usage explicită ViewState sau PrintState le suprascrie pe ambele, neparticiparea Intent contează ca vizibil, iar tabloul AS rulează ultimul
Editarea tablourilor ON și OFF nu este de ajuns pentru că PDFium împăturește BaseState, ambele tablouri, starea Usage a grupului și în final regulile auto-state AS într-un singur verdict pe care InspectOptionalContent îl reproduce pas cu pas

Al treilea pas este cel care arde oamenii. Un fișier salvat de un instrument de layout poartă adesea /ViewState /ON pe fiecare OCG, iar PDFium ignoră apoi tabloul /OFF editat cu grijă: salvarea reușește, fișierul se redeschide curat, iar stratul pictează în continuare. Pentru Print și Export, OcExplicitUsageState citește întâi PrintState sau ExportState și cade pe ViewState când intrarea specifică lipsește, deci un ViewState /ON singuratic fixează stratul și pentru tipărire. Conținutul marcat care face referință la un OCMD (§8.11.2.2) este apoi rezolvat contra acestor rezultate per grup, prin politica /P sau, când este prezentă, expresia de vizibilitate /VE

Cum listezi straturile pe care PDFium le va arăta efectiv?

TPdf.InspectOptionalContent întoarce un TPdfOptionalContentInventory al cărui tablou Groups poartă numărul de obiect al fiecărui OCG, numele, intent-urile, cele trei stări Usage, limba, intervalul de zoom, flag-ul Locked, indexul de grup radio și EffectiveVisible calculat. Metoda îl face întâi pe PDFium să salveze documentul curent din memorie, expandează fluxurile de obiecte și scanează rezultatul, astfel încât editările făcute mai devreme în sesiune se reflectă. Indexul de configurație 0 este întotdeauna dicționarul implicit /D, iar intrările lui /Configs urmează de la indexul 1; argumentul implicit de -1 selectează indexul 0. Un document fără /OCProperties face ca metoda să întoarcă False cu motivul în ErrorMessage în loc să ridice

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage are implicit ocuView; -1 selectează configurația 0, dicționarul /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;

Tabloul Memberships raportează fiecare OCMD cu Policy-ul lui (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), textul brut VisibilityExpression și propriul lui EffectiveVisible. Câteva reguli de margine sunt deliberate. /P are implicit /AnyOn, iar un OCMD fără grupuri contează ca vizibil. O referință la un număr de obiect care nu este un OCG cunoscut este tratată ca vizibilă în loc să prăbușească întreaga expresie. Evaluarea /VE se oprește la o adâncime de imbricare de 32 și tratează orice mai adânc ca ascuns, ceea ce împiedică o expresie ostilă sau autoreferențială să transforme inspecția într-un stack overflow

Scrierea unei noi stări de strat cu SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured primește un tablou de recorduri TPdfOptionalContentStateChange (număr de obiect de grup plus Visible) și scrie un document în care configurația selectată produce exact starea aceea. Configurația selectată primește /BaseState /ON plus tablouri complete /ON și /OFF care acoperă fiecare grup, iar fiecare OCG care are deja un dicționar Usage primește un ViewState explicit (sau PrintState / ExportState, după Options.Usage) care se potrivește cu noua lui stare. Cu TPdfOptionalContentConfigureOptions.Default, cheia /AS a configurației selectate este îndepărtată, astfel încât un eveniment de deschidere, tipărire sau export să nu poată răsturna straturile înapoi

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;

  // Configurația 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;

Calea de scriere păstrează ieșirea salvată a PDFium ca prefix identic byte cu byte și adaugă doar proprietarul de configurație rescris și obiectele OCG care poartă dicționare Usage, urmate de o secțiune xref nouă și un trailer. Înainte ca un singur byte să ajungă la destinație, rezultatul este redeschis într-un TPdf separat sub politica strictă de încărcare, iar metoda eșuează dacă tabelul de referințe încrucișate nu se validează. Overload-ul pe fișier merge un pas mai departe: scrie într-un fișier temporar lângă țintă și înlocuiește ținta doar după ce verificarea reușește, astfel încât o actualizare respinsă nu lasă niciodată în urmă un desen pe jumătate scris. Este aceeași abordare de revizuire incrementală verificată folosită de editorul de arbori de nume și de numere PDF din PDFium Component

Cum scrie SaveAsOptionalContentConfigured din PDFium Component un PDF Delphi cu straturi comutate: schimbările de stare și opțiunile intră, configurația selectată este rescrisă cu tablouri complete ON și OFF și stări Usage, revizuirea incrementală verificată este adăugată, iar o redeschidere strictă trebuie să valideze înainte să fie scris ceva
Salvarea configurată păstrează resalvarea proprie a PDFium ca prefix de byte, adaugă proprietarul de configurație rescris plus o secțiune xref nouă și redeschide rezultatul într-un TPdf separat înainte ca destinația să fie atinsă

Ce refuză să facă salvarea configurată?

Salvarea configurată refuză orice schimbare pe care documentul însuși o interzice sau nu o poate reprezenta în siguranță, iar fiecare refuz se întâmplă înainte ca destinația să fie atinsă. Un număr de obiect care nu este în /OCGs eșuează categoric. Schimbarea unui grup listat în tabloul /Locked al configurației eșuează, deși reformularea valorii lui curente este permisă. Cu EnforceRadioGroups activat, orice mulțime /RBGroups care ar ajunge cu mai mult de un membru vizibil este respinsă în loc să oprească în tăcere celelalte. Documentele criptate sunt respinse pentru că obiectele incrementale în text clar nu pot duce handler-ul de securitate activ. Documentele semnate ridică EPdfError dacă nu pasați AllowSignedDocument = True, fiindcă schimbarea a ceea ce arată o pagină poate rupe acoperirea semnăturii sau o politică de certificare

Porțile de refuz pe care SaveAsOptionalContentConfigured le aplică în PDFium Component înainte să scrie un PDF Delphi configurat: un număr de obiect din afara OCG-urilor eșuează, grupurile blocate eșuează, mulțimile RBGroups cu mai mult de un membru vizibil sunt respinse, documentele criptate nu pot duce obiecte incrementale în text clar, iar fișierele semnate cer AllowSignedDocument
Fiecare refuz se întâmplă înainte ca destinația să fie atinsă, iar motivul eșecului aterizează în Report.ErrorMessage în loc să lase în urmă un desen pe jumătate scris
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // scrie /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // prima intrare a lui /Configs, nu /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument rămâne False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // fișier semnat: nimic scris în Target
      Result := False;
    end;
  end;
end;

Cunoașteți compromisurile înainte să cuplați asta într-un job de batch. Revizuirea adăugată stă peste resalvarea completă a PDFium, nu peste byte-ii originali ai fișierului dvs., ceea ce este exact motivul pentru care input-ul semnat are nevoie de consimțământ explicit. Rescrierea normalizează de asemenea configurația selectată la /BaseState /ON, astfel încât o linie de bază /Unchanged sau /OFF a autorului este înlocuită de tablouri explicite cu aceeași vizibilitate rezultată. Îndepărtarea lui /AS elimină trucurile doar de tipar, precum un strat de filigran care apare doar pe hârtie; setați DisableAutomaticState la False ca să păstrați acele reguli, acceptând că vă pot suprascrie starea cerută pentru acel eveniment. Pe partea bună, PDF/A-2 (ISO 19005-2 clauza 6.9) și PDF/UA (ISO 14289-1 clauza 7.10) interzic ambele /AS în dicționarele de configurație, deci ieșirea implicită elimină o problemă pe care validarea de preflight PDF/A cu PDFium Component ar raporta-o altfel

Unde se încadrează controlul straturilor într-un viewer PDF Delphi

Într-un viewer, controlul straturilor este o listă de bifare condusă de inventar plus o reîncărcare a rezultatului salvat. Populați lista din Groups, dezactivați intrările care sunt Locked, tratați membrii care împart un RadioGroupIndex ca reciproc exclusivi, iar la aplicare scrieți într-un TMemoryStream și încărcați fluxul acela înapoi în TPdf ca vederea să picteze noua stare. Cablajul dintre TPdf și TPdfView este acoperit în construirea unui viewer PDF bogat în funcții cu PDFium VCL în Delphi. Licențierea, descărcările de probă și restul setului de funcții sunt pe pagina de produs PDFium Component pentru Delphi