Teknisk artikel

Växla PDF optional content-lager i Delphi med PDFium

PDFium Component styr PDF optional content-lager (OCG:er) i Delphi genom två TPdf-metoder: InspectOptionalContent listar varje lager tillsammans med den synlighet PDFium faktiskt renderar, och SaveAsOptionalContentConfigured skriver en verifierad kopia där de lager du väljer är slagna på eller av. Den andra metoden neutraliserar också Usage- och /AS-reglerna som annars tyst skulle dra tillbaka din ändring. Båda arbetar på dokumentet som redan är öppet i TPdf, så det finns ingen andra parser att hålla synkroniserad med vad visaren visar

Önskemålet kommer oftast från en CAD- eller GIS-verkstad: ritningssatsen levereras med mått, anteckningar och ett ritningshuvud på separata lager, och kunden vill ha en kopia med måtten dolda innan den går till en leverantör. PDFium renderar optional content korrekt, men dess publika ABI saknar funktioner för att räkna upp OCG:er, välja en konfiguration eller vända ett lagertillstånd. Så du går ner på objektnivå, redigerar /OCProperties, sparar, laddar om, och lagret finns fortfarande kvar. Orsaken är PDFiums synlighetslogik, och den är värd att förstå innan du rör en enda byte

Varför ändrar redigering av /ON och /OFF inte vad PDFium renderar?

Att redigera arrayerna /ON och /OFF i konfigurationsordboken räcker inte, för PDFium låter ett explicit tillstånd inuti OCG:ns egen /Usage-ordbok vinna över de arrayerna, och en /AS-autotillståndsregel kan sedan sänka båda. ISO 32000-1 §8.11.4 beskriver konfigurationer och usage-ordböcker som separata mekanismer; PDFiums renderare viker ihop dem till ett enda beslut, och InspectOptionalContent återskapar det i denna ordning:

  • Starta från konfigurationens /BaseState, där /ON och /Unchanged båda räknas som synliga och bara /OFF döljer
  • Tillämpa konfigurationens /ON-array, sedan dess /OFF-array, så att en grupp som finns i båda slutar dold
  • Tillämpa gruppens explicita Usage-tillstånd för den begärda användningen, som /Usage << /View << /ViewState /OFF >> >>, vilket sänker allt ovan
  • Behandla en grupp vars /Intent innehåller vare sig /View eller /All som synlig, eftersom den inte deltar i synlighet för view-intention
  • Till sist köra den valda konfigurationens /AS-array, vars poster för den matchande händelsen sätter tillståndet för de grupper de listar
Det femstegsvisibilitetsbeslut som PDFium Component upprepar för varje PDF optional content-grupp i Delphi: BaseState sätter startpunkten, konfigurationens ON- och OFF-arrayer tillämpas i ordning, en explicit Usage-post ViewState eller PrintState sänker båda, Intent-icke-deltagande räknas som synligt, och AS-arrayen körs sist
Att redigera ON- och OFF-arrayerna räcker inte för att PDFium viker ihop BaseState, båda arrayerna, gruppens Usage-tillstånd och till sist AS-autotillståndsreglerna till en dom som InspectOptionalContent återskapar steg för steg

Det tredje steget är det som bränner folk. En fil sparad av ett layoutverktyg bär ofta /ViewState /ON på varje OCG, och PDFium ignorerar då din noggrant redigerade /OFF-array: sparningen lyckas, filen öppnas rent, och lagret målar fortfarande. För Print och Export läser OcExplicitUsageState PrintState eller ExportState först och faller tillbaka på ViewState när den specifika posten saknas, så ett ensamt ViewState /ON fäster lagret även vid utskrift. Markerat innehåll som refererar en OCMD (§8.11.2.2) löses sedan mot dessa per grupp-resultat, genom /P-policyn eller, när den finns, /VE-synlighetsuttrycket

Hur listar du lagren som PDFium faktiskt visar?

TPdf.InspectOptionalContent returnerar en TPdfOptionalContentInventory vars Groups-array bär varje OCG:s objektnummer, namn, intentioner, de tre Usage-tillstånden, språk, zoomintervall, Locked-flagga, radiogruppindex och det beräknade EffectiveVisible. Metoden låter först PDFium spara det aktuella dokumentet i minnet, expanderar objektströmmar och skannar resultatet, så ändringar gjorda tidigare i sessionen speglas. Konfigurationsindex 0 är alltid standardordboken /D och posterna i /Configs följer från index 1; standardargumentet -1 väljer index 0. Ett dokument utan /OCProperties får metoden att returnera False med orsaken i ErrorMessage i stället för att kasta

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage har ocuView som standard; -1 väljer konfiguration 0, /D-ordboken
  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;

Arrayen Memberships rapporterar varje OCMD med sin Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), råtexten VisibilityExpression och sitt eget EffectiveVisible. Några kantregler är medvetna. /P har /AnyOn som standard, och en OCMD utan grupper räknas som synlig. En referens till ett objektnummer som inte är en känd OCG behandlas som synlig i stället för att fälla hela uttrycket. /VE-evalueringen stannar vid ett nästlingsdjup på 32 och behandlar allt djupare som dolt, vilket hindrar ett fientligt eller själv-refererande uttryck från att förvandla inspektionen till en stack overflow

Skriva ett nytt lagertillstånd med SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured tar en array av TPdfOptionalContentStateChange-poster (gruppobjektnummer plus Visible) och skriver ett dokument där den valda konfigurationen producerar exakt det tillståndet. Den valda konfigurationen får /BaseState /ON plus kompletta /ON- och /OFF-arrayer som täcker varje grupp, och varje OCG som redan har en Usage-ordbok får en explicit ViewState (eller PrintState / ExportState, enligt Options.Usage) som matchar sitt nya tillstånd. Med TPdfOptionalContentConfigureOptions.Default tas nyckeln /AS i den valda konfigurationen bort så att en open-, print- eller export-händelse inte kan vända tillbaka lagren

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 och 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;

Skrivvägen behåller PDFiums egna sparade utdata som en byte-för-byte-prefix och lägger bara till den omskrivna konfigurationsägaren och de OCG-objekt som bär Usage-ordböcker, följt av en ny xref-sektion och trailer. Innan en enda byte når din destination öppnas resultatet på nytt i en separat TPdf under den strikta laddningspolicyn, och metoden misslyckas om korsreferenstabellen inte validerar. Filöverlagringen går ett steg längre: den skriver till en tillfällig fil bredvid målet och ersätter målet först när verifieringen lyckats, så en avvisad uppdatering lämnar aldrig en halvskriven ritning efter sig. Det är samma verifierade inkrementella revisionsmetod som redigeraren för PDF-namnträd och nummerträd i PDFium Component använder

Hur SaveAsOptionalContentConfigured i PDFium Component skriver en Delphi-PDF med växlade lager: tillståndsändringar och optioner går in, den valda konfigurationen skrivs om med kompletta ON- och OFF-arrayer och Usage-tillstånd, den verifierade inkrementella revisionen läggs till, och en strikt återöppning måste validera innan något skrivs
Den konfigurerade sparningen behåller PDFiums egna omsparning som byteprefix, lägger till den omskrivna konfigurationsägaren plus en ny xref-sektion, och öppnar resultatet på nytt i en separat TPdf innan destinationen vidrörs

Vad vägrar den konfigurerade sparningen göra?

Den konfigurerade sparningen vägrar varje ändring dokumentet själv förbjuder eller inte säkert kan representera, och varje vägran sker innan destinationen vidrörs. Ett objektnummer som inte finns i /OCGs faller rakt av. Att ändra en grupp som listas i konfigurationens /Locked-array misslyckas, även om att återge dess aktuella värde är tillåtet. Med EnforceRadioGroups påslagen avvisas varje /RBGroups-mängd som skulle sluta med mer än en synlig medlem i stället för att tyst slå av de andra. Krypterade dokument avvisas för att inkrementella objekt i klartext inte kan bära den aktiva säkerhetshanteraren. Undertecknade dokument kastar EPdfError om du inte skickar AllowSignedDocument = True, eftersom en ändring av vad en sida visar kan bryta signaturtäckning eller en certifieringspolicy

Vägringssystemen som SaveAsOptionalContentConfigured tillämpar i PDFium Component innan en konfigurerad Delphi-PDF skrivs: ett objektnummer utanför OCG:erna faller, låsta grupper faller, RBGroups-mängder med mer än en synlig medlem avvisas, krypterade dokument kan inte bära inkrementella klartextobjekt, och undertecknade filer kräver AllowSignedDocument
Varje vägran sker innan destinationen vidrörs, och misslyckandeorsaken landar i Report.ErrorMessage i stället för att lämna en halvskriven ritning efter sig
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örsta posten i /Configs, inte /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument förblir False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // undertecknad fil: inget skrivet till Target
      Result := False;
    end;
  end;
end;

Känn avvägningarna innan du kopplar in detta i ett batchjobb. Den tillagda revisionen vilar ovanpå PDFiums fullständiga omsparning, inte dina ursprungliga filbyte, vilket är precis varför undertecknad indata kräver uttryckligt samtycke. Omskrivningen normaliserar också den valda konfigurationen till /BaseState /ON, så en upphovspersons /Unchanged eller /OFF-baslinje ersätts av explicita arrayer med samma resulterande synlighet. Att släppa /AS tar bort utskriftsverk-trick som ett vattenstämpellager som bara dyker upp på papper; sätt DisableAutomaticState till False för att behålla de reglerna, med vetskap om att de kan sänka ditt begärda tillstånd för den händelsen. På plussidan förbjuder både PDF/A-2 (ISO 19005-2 klausul 6.9) och PDF/UA (ISO 14289-1 klausul 7.10) /AS i konfigurationsordböcker, så standardutdatan tar bort ett fel som din PDF/A-preflightvalidering med PDFium Component annars skulle rapportera

Var lagrkontroll passar in i en Delphi-PDF-visare

I en visare är lagrkontroll en checklista driven av inventeringen plus en omloadning av det sparade resultatet. Fyll checklistan från Groups, inaktivera posterna som är Locked, behandla medlemmar som delar en RadioGroupIndex som ömsesidigt uteslutande, och vid verkställighet skriv till en TMemoryStream och ladda den strömmen tillbaka i TPdf så att vyn målar det nya tillståndet. Kopplingen mellan TPdf och TPdfView tas upp i att bygga en funktionsrik PDF-visare med PDFium VCL i Delphi. Licensiering, utvärderingsnedladdningar och resten av funktionsuppsättningen finns på produktsidan för PDFium Component for Delphi