Technisch artikel

PDF optional content-lagen schakelen in Delphi met PDFium

PDFium Component stuurt PDF optional content-lagen (OCGs) in Delphi aan via twee TPdf-methoden: InspectOptionalContent zet elke laag op een rijtje samen met de zichtbaarheid die PDFium werkelijk rendert, en SaveAsOptionalContentConfigured schrijft een geverifieerde kopie waarin de lagen die u kiest aan of uit staan. De tweede methode neutraliseert bovendien de Usage- en /AS-regels die uw bewerking anders geruisloos ongedaan zouden maken. Beide werken op het document dat al in TPdf open staat, dus er is geen tweede parser die synchroon moet blijven met wat de viewer toont

Het verzoek komt meestal van een CAD- of GIS-huis: de tekeningenset gaat de deur uit met maatlijnen, annotaties en een titelblok op aparte lagen, en de klant wil een kopie met de maatlijnen verborgen voordat die naar een leverancier gaat. PDFium rendert optional content correct, maar zijn publieke ABI heeft geen functie om OCGs op te sommen, een configuratie te kiezen of een laagstatus om te zetten. Dus u daalt af naar objectniveau, bewerkt /OCProperties, slaat op, laadt opnieuw, en de laag staat er nog steeds. De reden is de zichtbaarheidslogica van PDFium, en die is het waard te begrijpen voordat u ook maar één byte aanraakt

Waarom verandert het bewerken van /ON en /OFF niet wat PDFium rendert?

Het bewerken van de arrays /ON en /OFF van het configuratiewoordenboek is niet genoeg, want PDFium laat een expliciete status in het eigen /Usage-woordenboek van de OCG winnen van die arrays, en een auto-state-regel /AS kan daarna beide overrulen. ISO 32000-1 §8.11.4 beschrijft configuraties en usage-woordenboeken als aparte mechanismen; de renderer van PDFium vouwt ze samen tot één besluit, en InspectOptionalContent speelt het in deze volgorde na:

  • Begin bij de /BaseState van de configuratie, waar /ON en /Unchanged beide als zichtbaar tellen en alleen /OFF verbergt
  • Pas de array /ON van de configuratie toe, daarna zijn /OFF, zodat een groep die in beide staat uiteindelijk verborgen is
  • Pas de expliciete Usage-status van de groep toe voor de gevraagde usage, zoals /Usage << /View << /ViewState /OFF >> >>, die alles hierboven overschrijft
  • Beschouw een groep waarvan /Intent noch /View noch /All bevat als zichtbaar, want hij doet niet mee aan view-intent-zichtbaarheid
  • Draai ten slotte de array /AS van de geselecteerde configuratie, waarvan de invoeren voor de matchende event de status bepalen van de groepen die ze noemen
Het besluit in vijf stappen voor zichtbaarheid dat PDFium Component voor elke PDF optional content-groep in Delphi naspeelt: BaseState bepaalt de start, de configuratie-arrays ON en OFF worden op volgorde toegepast, een expliciete Usage-invoer ViewState of PrintState overschrijft beide, Intent-niet-deelname telt als zichtbaar, en de array AS draait als laatste
De arrays ON en OFF bewerken is niet genoeg omdat PDFium BaseState, beide arrays, de Usage-status van de groep en ten slotte de AS-auto-state-regels samenvouwt tot één oordeel dat InspectOptionalContent stap voor stap naspeelt

De derde stap is degene waar mensen op stuklopen. Een bestand dat een layout-tool opslaat draagt vaak op elke OCG een /ViewState /ON, en PDFium negeert dan uw zorgvuldig bewerkte array /OFF: het opslaan slaagt, het bestand heropent netjes, en de laag tekent gewoon. Voor Print en Export leest OcExplicitUsageState eerst PrintState of ExportState en valt hij terug op ViewState zodra de specifieke invoer ontbreekt, dus een eenzame ViewState /ON zet de laag ook voor het printen vast. Marked content die naar een OCMD verwijst (§8.11.2.2) wordt daarna tegen deze resultaten per groep opgelost, via het /P-beleid of, als het er is, de zichtbaarheidsexpressie /VE

Hoe zet u de lagen op een rij die PDFium werkelijk toont?

TPdf.InspectOptionalContent geeft een TPdfOptionalContentInventory terug waarvan de array Groups per OCG het objectnummer, de naam, de intents, de drie Usage-statussen, de taal, het zoombereik, de vlag Locked, de radiogroepindex en de berekende EffectiveVisible meedraagt. De methode laat PDFium eerst het huidige in-memory document opslaan, expandeert objectstreams en scant het resultaat, dus bewerkingen die eerder in de sessie zijn gedaan zijn verwerkt. Configuratie-index 0 is altijd het standaardwoordenboek /D en de invoeren van /Configs volgen vanaf index 1; het standaardargument van -1 kiest index 0. Een document zonder /OCProperties laat de methode False teruggeven met de reden in ErrorMessage in plaats van een exceptie

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage staat standaard op ocuView; -1 kiest configuratie 0, het /D-woordenboek
  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;

De array Memberships meldt elke OCMD met zijn Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), de rauwe tekst VisibilityExpression en zijn eigen EffectiveVisible. Een paar randregels zijn bewust zo. /P staat standaard op /AnyOn, en een OCMD zonder groepen telt als zichtbaar. Een verwijzing naar een objectnummer dat geen bekende OCG is, wordt als zichtbaar behandeld in plaats van de hele expressie te laten falen. De evaluatie van /VE stopt bij een nestdiepte van 32 en behandelt alles dieper als verborgen, wat voorkomt dat een vijandige of zelfverwijzende expressie inspectie laat omslaan in een stack overflow

Een nieuwe laagstatus schrijven met SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured neemt een array van records TPdfOptionalContentStateChange (objectnummer van de groep plus Visible) en schrijft een document waarin de geselecteerde configuratie precies die status oplevert. De geselecteerde configuratie krijgt /BaseState /ON plus volledige arrays /ON en /OFF die elke groep bestrijken, en elke OCG die al een Usage-woordenboek heeft krijgt een expliciete ViewState (of PrintState / ExportState, volgens Options.Usage) die bij zijn nieuwe status past. Met TPdfOptionalContentConfigureOptions.Default wordt de sleutel /AS van de geselecteerde configuratie verwijderd, zodat een open-, print- of export-event de lagen niet terug kan omgooien

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;

  // Configuratie 0, ocuView, DisableAutomaticState en 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;

Het schrijfpad houdt de eigen opgeslagen uitvoer van PDFium aan als byte-voor-byte-prefix en voegt alleen de herschreven configuratie-eigenaar en de OCG-objecten met Usage-woordenboeken toe, gevolgd door een nieuwe xref-sectie en trailer. Voordat er ook maar één byte uw bestemming bereikt, wordt het resultaat in een aparte TPdf heropend onder het strikte laadbeleid, en de methode faalt als de kruisverwijzingstabel niet valideert. De bestandsoverload gaat nog een stap verder: die schrijft naar een tijdelijk bestand naast het doel en vervangt het doel pas als de verificatie slaagt, dus een geweigerde update laat nooit een halfgeschreven tekening achter. Het is dezelfde aanpak van geverifieerde incrementele revisie die de PDF name tree- en number tree-editor in PDFium Component gebruikt

Hoe SaveAsOptionalContentConfigured in PDFium Component een Delphi-PDF schrijft met omgeschakelde lagen: statuswijzigingen en opties gaan erin, de geselecteerde configuratie wordt herschreven met volledige ON- en OFF-arrays en Usage-statussen, de geverifieerde incrementele revisie wordt toegevoegd, en een strikte heropening moet valideren voordat er iets wordt weggeschreven
De geconfigureerde opslag houdt de eigen hersave van PDFium aan als byte-prefix, voegt de herschreven configuratie-eigenaar plus een nieuwe xref-sectie toe en heropent het resultaat in een aparte TPdf voordat de bestemming wordt aangeraakt

Wat weigert de geconfigureerde opslag te doen?

De geconfigureerde opslag weigert elke wijziging die het document zelf verbiedt of niet veilig kan weergeven, en elke weigering gebeurt voordat de bestemming wordt aangeraakt. Een objectnummer dat niet in /OCGs staat faalt ronduit. Het wijzigen van een groep die in de array /Locked van de configuratie staat faalt, al mag het herbevestigen van zijn huidige waarde wel. Met EnforceRadioGroups aan wordt elke /RBGroups-verzameling die met meer dan één zichtbaar lid zou eindigen geweigerd in plaats van de anderen geruisloos uit te zetten. Versleutelde documenten worden geweigerd omdat plaintext-incrementele objecten de actieve security handler niet kunnen meedragen. Ondertekende documenten gooien EPdfError tenzij u AllowSignedDocument = True doorgeeft, want veranderen wat een pagina toont kan handtekeningdekking of een certificeringsbeleid breken

De weigerpoorten die SaveAsOptionalContentConfigured in PDFium Component toepast voordat er een geconfigureerde Delphi-PDF wordt geschreven: een objectnummer buiten OCGs faalt, vergrendelde groepen falen, RBGroups-verzamelingen met meer dan één zichtbaar lid worden geweigerd, versleutelde documenten kunnen geen plaintext-incrementele objecten meedragen, en ondertekende bestanden eisen AllowSignedDocument
Elke weigering gebeurt voordat de bestemming wordt aangeraakt, en de faalreden belandt in Report.ErrorMessage in plaats van een halfgeschreven tekening achter te laten
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // schrijft /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // eerste invoer van /Configs, niet /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument blijft False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // ondertekend bestand: niets naar Target geschreven
      Result := False;
    end;
  end;
end;

Ken de afwegingen voordat u dit in een batchtaak inbouwt. De toegevoegde revisie rust bovenop de volledige hersave van PDFium, niet op de oorspronkelijke bestandsbytes, en dat is precies waarom ondertekende invoer expliciete toestemming nodig heeft. De herschrijving normaliseert de geselecteerde configuratie bovendien naar /BaseState /ON, dus een baseline /Unchanged of /OFF van de auteur wordt vervangen door expliciete arrays met dezelfde resulterende zichtbaarheid. Het laten vallen van /AS schrapt print-only trucs zoals een watermerklaag die alleen op papier verschijnt; zet DisableAutomaticState op False om die regels te houden, accepterend dat ze uw gevraagde status voor dat event kunnen overrulen. Aan de positieve kant: PDF/A-2 (ISO 19005-2 clausule 6.9) en PDF/UA (ISO 14289-1 clausule 7.10) verbieden beide /AS in configuratiewoordenboeken, dus de standaarduitvoer schrapt één probleem dat uw PDF/A-preflightvalidatie met PDFium Component anders zou melden

Waar laagbeheer past in een Delphi-PDF-viewer

In een viewer is laagbeheer een checklist aangedreven door de inventaris plus een heropening van het opgeslagen resultaat. Vul de checklist vanuit Groups, maak de invoeren die Locked zijn onbruikbaar, behandel leden die een RadioGroupIndex delen als wederzijds uitsluitend, en schrijf bij toepassen naar een TMemoryStream en laad die stream terug in TPdf zodat de view de nieuwe status tekent. De samenhang tussen TPdf en TPdfView staat beschreven in een veelzijdige PDF-viewer bouwen met PDFium VCL in Delphi. Licenties, trial-downloads en de rest van de featureset staan op de productpagina van PDFium Component voor Delphi