Teknisk artikel

Styr PDF optional content-lag i Delphi med PDFium

PDFium Component styrer PDF optional content-lag (OCGs) i Delphi gennem to TPdf-metoder: InspectOptionalContent lister hvert lag sammen med den synlighed, PDFium reelt renderer, og SaveAsOptionalContentConfigured skriver en verificeret kopi, i hvilken de lag, du vælger, er slået til eller fra. Den anden metode neutraliserer også Usage- og /AS-reglerne, som ellers i det stille ville fortryde din redigering. Begge virker på dokumentet, der allerede er åbent i TPdf, så der er ingen anden parser at holde i sync med, hvad viewer'en viser

Forespørgslen kommer som regel fra et CAD- eller GIS-hus: tegningssættet sejler med mål, anmærkninger og en titelblok på separate lag, og kunden vil have en kopi med målene skjulte, før den går til en leverandør. PDFium renderer optional content korrekt, men dets offentlige ABI har ingen funktion til at enumerere OCGs, vælge en konfiguration eller vende en lag-tilstand. Så man går ned på objektniveau, redigerer /OCProperties, gemmer, genindlæser, og laget er der stadig. Grunden er PDFiums synlighedslogik, og den er værd at forstå, før man rører nogen bytes

Hvorfor ændrer redigering af /ON og /OFF ikke, hvad PDFium renderer?

At redigere /ON- og /OFF-arrays i konfigurationsdictionaryen er ikke nok, fordi PDFium lader en eksplicit tilstand inde i OCG'ens egen /Usage-dictionary vinde over de arrays, og en /AS auto-state-regel kan derefter overskrive begge. ISO 32000-1 §8.11.4 beskriver konfigurationer og usage dictionaries som separate mekanismer; PDFiums renderer folder dem sammen til én enkelt beslutning, og InspectOptionalContent reproducerer den i denne rækkefølge:

  • Start fra konfigurationens /BaseState, hvor /ON og /Unchanged begge tæller som synlige, og kun /OFF skjuler
  • Anvend konfigurationens /ON-array, derefter dens /OFF-array, så en gruppe opført i begge ender skjult
  • Anvend gruppens eksplicitte Usage-tilstand for den efterspurgte usage, såsom /Usage << /View << /ViewState /OFF >> >>, som overskriver alt ovenfor
  • Behandl en gruppe, hvis /Intent hverken indeholder /View eller /All, som synlig, da den ikke deltager i view-intent synlighed
  • Kør til sidst den valgte konfigurations /AS-array, hvis entries for det matchende event sætter tilstanden for de grupper, de lister
Den femtrins synlighedsbeslutning, som PDFium Component afspiller for hver PDF optional content-gruppe i Delphi: BaseState sætter starten, konfigurationens ON- og OFF-arrays anvendes i rækkefølge, en eksplicit Usage ViewState- eller PrintState-entry overskriver begge, Intent-nondeltagelse tæller som synlig, og AS-arrayet kører sidst
At redigere ON- og OFF-arrays er ikke nok, fordi PDFium folder BaseState, begge arrays, gruppens Usage-tilstand og til sidst AS auto-state-reglerne sammen til én dom, som InspectOptionalContent reproducerer trin for trin

Tredje trin er det, der brænder folk. En fil gemt af et layoutværktøj bærer ofte /ViewState /ON på hver OCG, og PDFium ignorerer så dit omhyggeligt redigerede /OFF-array: gemningen lykkes, filen genåbner rent, og laget maler stadig. For Print og Export læser OcExplicitUsageState PrintState eller ExportState først og falder tilbage til ViewState, når den specifikke entry mangler, så en enlig ViewState /ON fastholder laget til udskrivning også. Marked content, der refererer en OCMD (§8.11.2.2), resolves derefter mod disse pr.-gruppe-resultater, gennem /P-politikken eller, når til stede, /VE-synlighedsudtrykket

Hvordan lister man de lag, PDFium reelt vil vise?

TPdf.InspectOptionalContent returnerer et TPdfOptionalContentInventory, hvis Groups-array bærer hver OCG's objektnummer, navn, intents, de tre Usage-tilstande, sprog, zoom-område, Locked-flag, radio-gruppe-indeks og den beregnede EffectiveVisible. Metoden får først PDFium til at gemme det aktuelle in-memory dokument, ekspanderer object streams og scanner resultatet, så redigeringer foretaget tidligere i sessionen afspejles. Konfigurationsindeks 0 er altid standard /D-dictionaryen, og /Configs-entryerne følger fra indeks 1; standardargumentet -1 vælger indeks 0. Et dokument uden /OCProperties får metoden til at returnere False med årsagen i ErrorMessage frem for at rejse

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage står som standard til ocuView; -1 vælger konfiguration 0, /D-dictionaryen
  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;

Memberships-arrayet rapporterer hver OCMD med dens Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), den rå VisibilityExpression-tekst og dens egen EffectiveVisible. Et par kantregler er bevidste. /P står som standard til /AnyOn, og en OCMD uden grupper tæller som synlig. En reference til et objektnummer, der ikke er en kendt OCG, behandles som synlig frem for at få hele udtrykket til at fejle. /VE-evalueringen stopper ved en nesting-dybde på 32 og behandler alt dybere som skjult, hvilket forhindrer et fjendtligt eller selvrefererende udtryk i at gøre inspektionen til en stack overflow

At skrive en ny lag-tilstand med SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured tager et array af TPdfOptionalContentStateChange-records (gruppeobjektnummer plus Visible) og skriver et dokument, i hvilket den valgte konfiguration producerer præcis den tilstand. Den valgte konfiguration får /BaseState /ON plus komplette /ON- og /OFF-arrays, der dækker hver gruppe, og hver OCG, der allerede har en Usage-dictionary, modtager en eksplicit ViewState (eller PrintState / ExportState, følgende Options.Usage), der matcher dens nye tilstand. Med TPdfOptionalContentConfigureOptions.Default fjernes /AS-nøglen fra den valgte konfiguration, så et open-, print- eller export-event ikke kan vende lagene tilbage

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;

  // Konfiguration 0, ocuView, DisableAutomaticState og 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;

Skrivestien beholder PDFiums egen gemte output som byte-for-byte-præfiks og føjer kun den omskrevne configuration owner og de OCG-objekter, der bærer Usage-dictionaries, efterfulgt af en ny xref-sektion og trailer. Før en eneste byte når din destination, genåbnes resultatet i en separat TPdf under den strenge load-politik, og metoden fejler, hvis cross-reference-tabellen ikke validerer. Fil-overloaden går et skridt videre: den skriver til en midlertidig fil ved siden af målet og erstatter målet først, efter verifikationen lykkes, så en afvist opdatering aldrig efterlader en halvfærdig tegning. Det er samme verificerede inkrementelle revisionstilgang, som PDF name tree- og number tree-editoren i PDFium Component bruger

Hvordan SaveAsOptionalContentConfigured i PDFium Component skriver en Delphi-PDF med lag vendt: tilstandsændringer og options går ind, den valgte konfiguration omskrives med komplette ON- og OFF-arrays og Usage-tilstande, den verificerede inkrementelle revision tilføjes, og et strict reopen skal validere, før noget skrives
Den konfigurerede gemning beholder PDFiums egen re-gemning som byte-præfiks, føjer den omskrevne configuration owner plus en ny xref-sektion til og genåbner resultatet i en separat TPdf, før destinationen røres

Hvad nægter den konfigurerede gemning at gøre?

Den konfigurerede gemning nægter enhver ændring, dokumentet selv forbyder eller ikke kan repræsentere sikkert, og hver afvisning sker, før destinationen røres. Et objektnummer, der ikke er i /OCGs, fejler kategorisk. At ændre en gruppe opført i konfigurationens /Locked-array fejler, selvom det er tilladt at gentage dens aktuelle værdi. Med EnforceRadioGroups slået til afvises ethvert /RBGroups-sæt, der ville ende med mere end ét synligt medlem, i stedet for stille at slå de andre fra. Krypterede dokumenter afvises, fordi klartekst-inkrementelle objekter ikke kan bære den aktive security handler. Signerede dokumenter rejser EPdfError, medmindre du giver AllowSignedDocument = True med, da det at ændre, hvad en side viser, kan knække signaturdækning eller en certificeringspolitik

Afvisningsgaterne, som SaveAsOptionalContentConfigured anvender i PDFium Component før skrivning af en konfigureret Delphi-PDF: et objektnummer uden for OCGs fejler, låste grupper fejler, RBGroups-sæt med mere end ét synligt medlem afvises, krypterede dokumenter kan ikke bære klartekst-inkrementelle objekter, og signerede filer kræver AllowSignedDocument
Enhver afvisning sker, før destinationen røres, og fejlårsagen lander i Report.ErrorMessage i stedet for at efterlade en halvfærdig tegning
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // skriver /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // første entry af /Configs, ikke /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument forbliver False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // signeret fil: intet skrevet til Target
      Result := False;
    end;
  end;
end;

Kend afvejningerne, før du kobler dette ind i et batch-job. Den tilføjede revision sidder oven på PDFiums fulde re-gemning, ikke dine originale filbytes, hvilket er præcis, hvorfor signeret input behøver eksplicit samtykke. Omskrivningen normaliserer også den valgte konfiguration til /BaseState /ON, så en forfatters /Unchanged- eller /OFF-baseline erstattes af eksplicitte arrays med samme resulterende synlighed. At droppe /AS fjerner kun-udskrivning-tricks som et vandmærke-lag, der kun dukker op på papir; sæt DisableAutomaticState til False for at beholde de regler og acceptere, at de kan overskrive din efterspurgte tilstand for det event. På plussiden forbyder både PDF/A-2 (ISO 19005-2 clause 6.9) og PDF/UA (ISO 14289-1 clause 7.10) /AS i konfigurationsdictionaries, så standardoutput fjerner ét problem, som din PDF/A preflight-validering med PDFium Component ellers ville rapportere

Hvor lag-styring passer ind i en Delphi PDF-viewer

I en viewer er lag-styring en tjekliste drevet af inventaret plus en genindlæsning af det gemte resultat. Udfyld tjeklisten fra Groups, deaktivér de entries, der er Locked, behandl medlemmer, der deler et RadioGroupIndex, som gensidigt udelukkende, og ved anvendelse skriv til en TMemoryStream og indlæs den stream tilbage i TPdf, så visningen maler den nye tilstand. Koblingen mellem TPdf og TPdfView er dækket i at bygge en funktionsrig PDF-viewer med PDFium VCL i Delphi. Licensering, prøvedownloads og resten af featuresættet findes på PDFium Component for Delphi-produktsiden