Technisch artikel

PDF optional content-lagen schakelen in Delphi met PDFium

Een tekeningenset komt binnen met maatlijnen op een eigen laag, en de voor de hand liggende manier om ze te verbergen is de referentie van die groep uit de /ON-array van de standaardconfiguratie naar /OFF te verplaatsen. Het bestand wordt opgeslagen en heropend, en de maatlijnen worden nog steeds dwars door het titelblok gedrukt. De oorzaak zit één object verderop, in de groepdictionary: /Usage << /View << /ViewState /ON >> >>, een expliciete status die de renderer vóór de configuratie-arrays leest

De status van een laag kan op meerdere plekken in een PDF staan, en een bewerking van /OCProperties raakt er maar één ervan. De PDFium-engine van Google rendert optional content correct, maar zijn publieke C-API heeft geen functie om optional content-groepen (OCGs) op te sommen, een configuratie te kiezen of een groep aan en uit te zetten. Delphi-code die lagen aanstuurt, moet de objecten zelf herschrijven en de beslisvolgorde van de renderer exact naspelen, anders meldt zijn laaglijst de ene status terwijl de pagina de andere laat zien

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

De arrays /ON en /OFF zijn pas de tweede van vijf stappen, en twee latere stappen kunnen ze oversturen. ISO 32000-1 §8.11.4.3 definieert de optional content-configuration-dictionary met /BaseState, /ON, /OFF, /Locked, /RBGroups en /AS, terwijl §8.11.4.4 de Usage-dictionary definieert die een OCG kan meedragen. In de specificatie beschrijft Usage waar een groep voor dient, en de auto-state-invoeren in /AS zetten die beschrijving om in ON of OFF. PDFium neemt een kortere route: als hij voor een doel rendert, wint de eigen expliciete status van de groep voor dat doel. Vouwt u dat samen, dan ziet de volgorde er zo uit:

  • Begin bij de /BaseState van de configuratie, waar /ON en /Unchanged allebei als zichtbaar tellen en alleen /OFF verbergt
  • Pas de /ON-array toe, dan de /OFF-array, zodat een groep die in beide staat uiteindelijk verborgen is
  • Pas de expliciete Usage-status van de groep voor het gevraagde doel toe, zoals /Usage << /View << /ViewState /OFF >> >>, die alles hierboven overstijgt
  • Beschouw een groep waarvan /Intent noch /View noch /All bevat als zichtbaar, want hij doet niet mee aan view-intent-zichtbaarheid
  • Draai de /AS-array van de geselecteerde configuratie als laatste, waarbij invoeren voor de matchende gebeurtenis de status van de groepen die ze noemen bepalen
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

Stap drie verklaart het openingsbestand: sommige exporteurs schrijven /ViewState /ON in elke groep die ze maken, en vanaf dat moment verbergt geen enkele arraybewerking die lagen nog. Printen en export volgen dezelfde regel met één draai: de renderer leest eerst /PrintState of /ExportState en valt terug op /ViewState als de specifieke invoer ontbreekt. Een eenzame /ViewState /ON spijkert de laag dus ook op papier vast. Stap vier is stiller. Een groep getagd /Intent /Design valt buiten view-intent-zichtbaarheid en schildert ongeacht de arrays, terwijl een groep zonder /Intent-sleutel als /View geldt, de standaard die de specificatie geeft

Lagen en hun gerenderde status tonen met InspectOptionalContent

PDFium Component speelt die volgorde na in TPdf.InspectOptionalContent. De methode vult een TPdfOptionalContentInventory en geeft True terug bij succes; zijn parameter Usage staat standaard op ocuView en zijn parameter ConfigurationIndex op -1, wat index 0 kiest. Configuratie-index 0 is de standaard /D-dictionary, en de entries van /Configs volgen vanaf index 1 in bestandsvolgorde. Vóór de scan laat de methode PDFium het open document in het geheugen opslaan en pakt hij eventuele object streams uit, zodat bewerkingen van eerder in de sessie in het resultaat meekomen. Een bestand zonder /OCProperties, of een configuratie-index buiten het beschikbare bereik, laat de aanroep False teruggeven met de reden in ErrorMessage; voor geen van beide wordt een exception geworpen

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  Cfg: TPdfOptionalContentConfiguration;
  G: TPdfOptionalContentGroup;
begin
  // Usage staat standaard op ocuView; -1 kiest configuratie 0, de /D-dictionary
  if not Pdf.InspectOptionalContent(Inv) then
  begin
    Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
    Exit;
  end;
  Cfg := Inv.Configurations[Inv.SelectedConfigurationIndex];
  Memo1.Lines.Add(Format('Configuration %d "%s", %d groups, /AS present: %s',
    [Inv.SelectedConfigurationIndex, string(Cfg.Name), Length(Inv.Groups),
     BoolToStr(Cfg.HasAutomaticState, True)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %-24s visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

Elke TPdfOptionalContentGroup draagt zijn objectnummer, naam, Intents en de drie expliciete statussen ViewState, PrintState en ExportState, elk één van ocsUnspecified, ocsOn of ocsOff. Leest een status ocsOn en zit de groep in de /OFF-array, dan heeft u de valkuil uit de openingsparagraaf te pakken. Locked en RadioGroupIndex komen uit de geselecteerde configuratie, en RadioGroupIndex is -1 voor een groep die bij geen enkele /RBGroups-verzameling hoort

De rest van de Usage-dictionary wordt gemeld zoals het bestand ze aangeeft: Language en LanguagePreferred, UserType en UserNames, PageElementSubtype, en MinimumZoom en MaximumZoom achter een HasZoomRange-vlag. EffectiveVisible hangt van geen van allen af. Zoomafhankelijke weergave is aan uw viewer. Elke TPdfOptionalContentConfiguration legt zijn naam, BaseState, arrayinhoud en RadioGroups bloot, plus OrderGroupObjectNumbers, dat /Order afvlakt tot groepsverwijzingen en de tekstlabels van geneste labelarrays overslaat

Print en export kunnen het met het scherm oneens zijn

Het argument Usage verandert het antwoord, en dat is precies het doel. Onder ocuPrint leest de inventaris /PrintState met zijn terugval op /ViewState, en alleen /AS-invoeren waarvan /Event /Print is doen mee. Een watermerklaag die alleen op papier moet verschijnen meldt zich onder ocuView dus verborgen en onder ocuPrint zichtbaar. De twee passages vergelijken vindt zulke lagen voordat een afdruk iemand verrast

procedure TFormMain.ReportPrintOnlyLayers;
var
  OnScreen, OnPaper: TPdfOptionalContentInventory;
  I: Integer;
begin
  if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
     not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
    Exit;
  // Beide passages lezen dezelfde /OCGs-array, dus de groepsvolgorde klopt
  for I := 0 to High(OnScreen.Groups) do
    if OnScreen.Groups[I].EffectiveVisible <> OnPaper.Groups[I].EffectiveVisible then
      Memo1.Lines.Add(Format('%s: screen=%s, print=%s',
        [string(OnScreen.Groups[I].Name),
         BoolToStr(OnScreen.Groups[I].EffectiveVisible, True),
         BoolToStr(OnPaper.Groups[I].EffectiveVisible, True)]));
end;

Hoe worden OCMD-zichtbaarheidsuitdrukkingen geëvalueerd?

Een optional content membership dictionary (OCMD, ISO 32000-1 §8.11.2.2) koppelt een stuk inhoud via een /P-policy aan meerdere groepen, of via een /VE-visibility-expressie die groepen met /And, /Or en /Not combineert. De specificatie geeft /P een standaard van /AnyOn, laat /VE voorgaan op /OCGs en /P als hij aanwezig is, en zegt dat een OCMD zonder groepen niets doet aan zichtbaarheid

De Memberships-array van de inventaris volgt die regels tegen de per-groep resultaten van dezelfde passage, dus de usage die u vroeg loopt door. Elk lidmaatschap meldt zijn groepsobjectnummers, Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff, of ocmpUnknown voor een naam buiten die vier), de rauwe VisibilityExpression-tekst en zijn eigen EffectiveVisible. Een OCMD zonder groepen geldt als zichtbaar. Ook een verwijzing naar een objectnummer dat geen bekende OCG is, geldt als zichtbaar, zodat één losse verwijzing niet alles eromheen verbergt. /VE-evaluatie stopt op een nestingsdiepte van 32 en behandelt alles dieper als verborgen, zodat een zichzelf refererende expressie de stack niet kan overlopen. De scan somt elke OCMD op die als indirect object met /Type /OCMD is opgeslagen, object streams incluis

Een nieuwe laagstatus schrijven met SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured neemt een array van TPdfOptionalContentStateChange-records, elk met een GroupObjectNumber en een Visible-vlag. Hij schrijft een kopie waarin de geselecteerde configuratie onder de vijfstappenorder precies die status oplevert. Groepen die u niet noemen houden de zichtbaarheid die de inventaris voor hen berekende

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer;
  ForPrint: Boolean);
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;

  Options := TPdfOptionalContentConfigureOptions.Default;
  if ForPrint then
  begin
    Options.Usage := ocuPrint;         // schrijft /Print << /PrintState ... >>
    Options.ConfigurationIndex := 1;   // eerste /Configs-entry, /D blijft zoals hij was
  end;

  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;

De uitvoer houdt het document dat PDFium zelf opslaat als byte-voor-byte-prefix en voegt alleen de herschreven configuratie-eigenaar en de OCG-objecten die Usage-dictionaries dragen toe, gevolgd door een nieuwe xref-sectie en trailer. Voordat ook maar iets uw bestemming bereikt, wordt het resultaat in een aparte TPdf onder het strikte laadbeleid heropend, en de methode faalt als die laad niet slaagt of de cross-reference-tabel niet valideert. De bestandsoverload schrijft naar een tijdelijk bestand naast het doel en vervangt het doel pas na verificatie, dus een geweigerde update laat de vorige tekening onaangeroerd. Het is dezelfde geverifieerde incrementele-revisiebenadering die de PDF name tree- en number tree-editor in PDFium Component gebruikt. Bevestig een printpreset door de uitvoer opnieuw te laden en InspectOptionalContent(Inv, ocuPrint, 1) aan te roepen; de groepen horen dan de statussen te melden die u doorgaf

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 verbiedt of niet veilig kan dragen, en elke weigering gebeurt voordat de bestemming wordt weggeschreven. De controles draaien in vaste volgorde. Een ondertekend document werpt EPdfError voordat er iets anders gebeurt, tenzij u AllowSignedDocument = True doorgeeft. Een versleuteld document wordt als volgende geweigerd, want plaintext-incrementele objecten kunnen de actieve security handler niet dragen. Daarna wordt elke gevraagde wijziging gecontroleerd: een objectnummer dat niet in /OCGs staat faalt ronduit, en een wijziging aan een groep die in de /Locked-array van de configuratie staat faalt, al zal het herstellen van zijn huidige waarde doorgaan. Ten slotte wordt, met EnforceRadioGroups aan, een /RBGroups-verzameling die met meer dan één zichtbaar lid zou eindigen geweigerd. Elke weigering behalve de signatuur geeft False terug met de reden in Report.ErrorMessage

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; de reden belandt in Report.ErrorMessage, of in het EPdfError-bericht als het bestand is ondertekend

Twee van die poorten gaan verder dan de specificatie strikt eist. §8.11.4.3 beschrijft /Locked als bescherming tegen wijzigingen via de gebruikersinterface van de viewer en beschrijft radioknopgroepen als het uitschakelen van de andere leden als er één wordt aangezet. PDFium Component past beide ook toe op programmatische wijzigingen, en bij radiogroepen weigert hij in plaats van leden uit te kiezen om uit te zetten. Gevraagd om twee leden van een wederzijds uitsluitende verzameling kan de bibliotheek niet weten welke u bedoelde, en een gok zou uw verzoek tegenspreken. Zet de anderen uit in dezelfde Changes-array en de opslag gaat door

De poort voor ondertekende documenten bestaat omdat de toegevoegde revisie bovenop de volledige hersave van PDFium zit, wat een nieuwe bytereeks is; de byte-range die een bestaande handtekening dekt, kan niet worden verondersteld te overleven. Geef AllowSignedDocument = True alleen door als het verlies van die handtekening acceptabel is

Drie neveneffecten tellen in een batchtaak. De herschrijving normaliseert de geselecteerde configuratie naar /BaseState /ON, dus een baseline /Unchanged of /OFF van de auteur wordt expliciete arrays met dezelfde resulterende zichtbaarheid. /AS laten vallen verwijdert ook print-only trucs zoals een watermerklaag die alleen op papier verschijnt; zet DisableAutomaticState op False om die regels te houden en accepteer dat ze uw gevraagde status voor hun gebeurtenis kunnen oversturen. En de arrays /ON en /OFF worden geschreven vanuit de statussen berekend voor Options.Usage, dus een printpreset die in configuratie 0 wordt opgeslagen verandert ook wat het scherm toont voor elke groep zonder Usage-dictionary. Houd printpresets in een /Configs-entry en laat /D aan het scherm. Aan de compliance-kant verbieden PDF/A-2 (ISO 19005-2 clause 6.9) en PDF/UA (ISO 14289-1 clause 7.10) allebei /AS in configuratiedictionaries, dus de standaarduitvoer ruimt één bevinding op die uw PDF/A-preflightvalidatie met PDFium Component anders zou melden

Laagwijzigingen toepassen binnen een Delphi-viewer

In een viewer is laagbeheer een checklist gebouwd vanuit de inventaris, gevolgd door een opslag naar het geheugen en een herlading. Vul de checklist vanuit Groups, schakel entries uit waarvan de Locked-vlag staat, behandel leden die een RadioGroupIndex delen als wederzijds uitsluitend, en schrijf bij toepassen naar een TMemoryStream via de stream-overload. TPdf.LoadDocument verwacht een inactieve component, dus zet hem eerst uit; de gebufferde laad kopieert de bytes, zodat u de stream meteen kunt vrijgeven

procedure TFormMain.ApplyLayerChecklist(
  const Changes: TPdfOptionalContentStateChanges);
var
  Report: TPdfOptionalContentConfigureReport;
  Output: TMemoryStream;
  KeepPage: Integer;
begin
  Output := TMemoryStream.Create;
  try
    try
      if not Pdf.SaveAsOptionalContentConfigured(Output, Changes,
        TPdfOptionalContentConfigureOptions.Default, Report) then
      begin
        ShowMessage('Layer update rejected: ' + Report.ErrorMessage);
        Exit;
      end;
    except
      on E: EPdfError do
      begin
        ShowMessage(E.Message);       // ondertekend bestand: Output is nog leeg
        Exit;
      end;
    end;
    KeepPage := Pdf.PageNumber;
    Pdf.Active := False;
    Pdf.LoadDocument(Output);         // Buffered staat standaard op True
    Pdf.PageNumber := KeepPage;
  finally
    Output.Free;
  end;
end;

Na de herlading schildert PDFium vanuit de nieuwe configuratie, dus het scherm en de volgende InspectOptionalContent-aanroep zijn het eens. De bekabeling tussen TPdf en TPdfView staat in een veelzijdige PDF-viewer bouwen met PDFium VCL in Delphi. Heeft een document nog geen lagen en moet u ze toevoegen, dan loopt PDF-lagen maken met OCG en OCMD in Delphi door SaveAsOptionalContentCreated heen

Prijzen, de trialbuild en de andere documentfuncties van de component staan vermeld op de productpagina van PDFium Component voor Delphi