Teknisk artikel

Skapa PDF-lager (OCG och OCMD) i Delphi med PDFium

PDFium Component skapar nya PDF-lager från Delphi via TPdf.SaveAsOptionalContentCreated. I ett och samma verifierade sparande lägger metoden till optional content-grupper (OCG:er), optional content-medlemskapsordböcker (OCMD:er) och anroparlevererad ritning wrappad i Form XObjects vars /OC-post pekar på en ny grupp, så att PDFium och andra visare visar eller döljer den ritningen när lagret växlas

Förfrågan är sällan abstrakt. Ett anläggningsritningsteam vill ha metriska och imperialiska mått på samma ritningsblad, med exakt en uppsättning synlig. Ett tryckeri vill ha ett skärlinjeöverdrag som syns på skärmen men kan stängas av före korrekturet. Ingen av filerna har några lager ännu, så det finns ingenting att växla. Att växla befintliga lager är ett annat jobb, täckt i att växla befintliga PDF optional content-lager med SaveAsOptionalContentConfigured; den här artikeln handlar om att skapa lagren från början

Varför kan du inte bara sätta /OC på ett sidobjekt med PDFium?

PDFium saknar en publik funktion för att fästa optional content på ett befintligt sidobjekt. De publika rubrikerna innehåller ingen FPDFPageObj_SetOC eller något motsvarande, så den vanliga redigera-sedan-FPDFPage_GenerateContent-vägen kan inte producera styrt innehåll. Porten måste skrivas in i filstrukturen, och den enda frågan är vilken struktur PDFium hedrar när det renderar

ISO 32000-1 §8.11.3 ger tre sätt att göra grafik valfri, och PDFium hanterar dem olika:

  • Avsnitt i sidinnehållsströmmar wrappade i markerat innehåll /OC /Name BDC ... EMC, kontrollerat per sidobjekt under renderingen
  • Form XObjects och bild-XObjects som bär en /OC-post i sin egen ordbok, kontrollerade när XObject:et målas
  • Annoteringar med en /OC-post i annoteringsordboken, som PDFium:s renderingsväg inte alls konsulterar, så en styrd annotering förblir synlig oavsett lagerläget

Markera-innehåll-inpackning betyder att skriva om befintliga innehållsströmmar operator för operator, och annoteringar utesluts av renderaren. Det lämnar Form XObject-vägen, och det är den PDFium Component använder: din ritning hamnar i ett nytt Form XObject med /OC pekande på gruppen, formen registreras i sidans /Resources /XObject-ordbok, och en ny innehållsström som håller en enda /Name Do fogas till sidans /Contents. Det befintliga sidinnehållet parsas eller skrivs aldrig om

Vägar för optional content-styrning i ISO 32000-1 under PDFium: markerat innehåll mellan OC BDC och EMC kontrolleras per sidobjekt, ett Form XObject med en /OC-post kontrolleras när det målas och är vägen PDFium Component skriver från Delphi, medan annoterings-/OC aldrig konsulteras
PDFium kontrollerar optional content för markerade innehållsavsnitt och XObject-ordböcker men inte för annoteringar, vilket är varför SaveAsOptionalContentCreated styr den levererade ritningen genom ett Form XObject och en Do-operator

Att beskriva grupper och styrda former i Delphi

TPdf.SaveAsOptionalContentCreated tar tre arrayer och returnerar en rapport. Båda overloaderna, en som skriver till en TStream och en till ett filnamn, delar samma parameterlista: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport, och en valfri AllowSignedDocument: Boolean = False. Metoden anlände i PDFium Component v3.154.0

Varje TPdfOptionalContentCreateGroup har en Name, en valfri Creator, en Intent av pociView eller pociDesign, en flagga InitialStateVisible, en flagga Locked och en RadioTag. Grupper som delar samma RadioTag landar i en /RBGroups-array, vilket är hur en visare vet att att slå på den ena slår av de andra. Varje TPdfOptionalContentCreateForm namnger sin styrande grupp via GroupIndex (ett nollbaserat index i din Groups-array), ett 1-baserat PageNumber, de råa innehållsströmsbytena, en begränsningsruta, en valfri matris och ett valfritt ResourceName

var
  Pdf: TPdf;
  Groups: TPdfOptionalContentCreateGroups;
  Forms: TPdfOptionalContentCreateForms;
  Report: TPdfOptionalContentCreateReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'sheet-a1.pdf';
    Pdf.Active := True;

    SetLength(Groups, 2);            // SetLength nollfyller posterna
    Groups[0].Name := 'Dimensions (mm)';
    Groups[0].Intent := pociView;
    Groups[0].InitialStateVisible := True;
    Groups[0].RadioTag := 'Units';
    Groups[1].Name := 'Dimensions (in)';
    Groups[1].Intent := pociView;
    Groups[1].InitialStateVisible := False;
    Groups[1].RadioTag := 'Units';

    SetLength(Forms, 2);
    Forms[0].GroupIndex := 0;
    Forms[0].PageNumber := 1;
    Forms[0].Content := TEncoding.ASCII.GetBytes(
      '0 0 1 RG 1.5 w 72 120 m 540 120 l S');
    Forms[0].BBoxRight := 612;
    Forms[0].BBoxTop := 792;
    Forms[0].ResourceName := 'DimMetric';
    Forms[1] := Forms[0];
    Forms[1].GroupIndex := 1;
    Forms[1].Content := TEncoding.ASCII.GetBytes(
      '1 0 0 RG 1.5 w 72 140 m 540 140 l S');
    Forms[1].ResourceName := 'DimImperial';

    if not Pdf.SaveAsOptionalContentCreated('sheet-a1-layers.pdf',
      Groups, nil, Forms, Report) then
      raise Exception.Create(Report.ErrorMessage);
  finally
    Pdf.Free;
  end;
end;

Notera att formulärets innehåll är ren vektorsritning. Komponenten skriver bara en tom /Resources-ordbok på varje form, så innehållsströmmen måste vara självförsörjande: enhetsfärger, banor, klippning och transformeringar fungerar, men allt som behöver en namngiven resurs (ett typsnitt för Tf, en bild för Do, en ExtGState för gs) har inget att slå upp mot. Lämnar du ResourceName tomt namns formulären FmOc1, FmOc2 och så vidare; ett anpassat namn får bara innehålla bokstäver, siffror, understrykning, minus och punkt

Vad lägger en OCMD till över en vanlig grupp?

En optional content-medlemskapsordbok gör synligheten beroende av flera grupper på en gång. ISO 32000-1 §8.11.2.2 definierar två former: en /OCs-lista med en /P-policy som /AnyOn eller /AllOn, och ett /VE-synlighetsuttryck byggt av /And, /Or och /Not i specifikationen. PDFium Component tar en TPdfOptionalContentCreateMembership med MemberGroupIndices, en Policy och en Expression; när Expression.GroupIndices inte är tomt vinner uttrycket och /VE skrivs jämte /OCs

procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
  SetLength(Memberships, 2);

  // Synlig när någon av enhetslagren är på: /OCs [...] /P /AnyOn
  SetLength(Memberships[0].MemberGroupIndices, 2);
  Memberships[0].MemberGroupIndices[0] := 0;
  Memberships[0].MemberGroupIndices[1] := 1;
  Memberships[0].Policy := ocmpAnyOn;

  // Synlig bara medan det imperiska lagret är av: /VE [ /Not ref ]
  SetLength(Memberships[1].MemberGroupIndices, 1);
  Memberships[1].MemberGroupIndices[0] := 1;
  Memberships[1].Expression.Kind := poceNot;
  SetLength(Memberships[1].Expression.GroupIndices, 1);
  Memberships[1].Expression.GroupIndices[0] := 1;
end;

Den stödda delmängden är medvetet smal. Policyer begränsas till ocmpAllOn och ocmpAnyOn; ocmpAnyOff och ocmpAllOff avvisas. Uttryck är en nivå djupa, med poceAll, poceAny eller poceNot över skapade grupper, och /Not kräver exakt ett grupindex. poceAll serialiseras som /And, poceAny som /Or och poceNot som /Not, de enda tre operatorer §8.11.2.2 definierar. Versioner före 3.181.2 skrev /All och /Any, vilka PDFium utvärderar som dolda, så har du skapat OCMD:er med ett äldre bygge, återskapa dem i stället för att lita på uttrycket. Varje medlemskap behöver också minst en post i MemberGroupIndices, även när ett uttryck finns, eftersom den listan blir /OCs-arrayen. Objektnumren för de nya ordböckerna kommer tillbaka i Report.CreatedMembershipObjectNumbers

Optional content-medlemskapsordböcker skapade av PDFium Component i Delphi: en /OCs-lista med en /AnyOn- eller /AllOn-policy utvärderar grupplägen tillsammans, medan ett /VE-synlighetsuttryck som /Not på en grupp styr innehåll med And-, Or- och Not-logik en nivå djupt
Policyer utvärderar /OCs-listan medan /VE-uttryck kombinerar grupper med /And, /Or och /Not - antingen sättet behöver medlemskapet minst en medlemsgrupp att referera

Två beteenden är värda att känna innan du bygger vidare på OCMD:er. Först refererar de styrda formerna i samma anrop bara grupper: TPdfOptionalContentCreateForm.GroupIndex indexerar Groups, inte Memberships, så en form kan inte styras av en nyskapad OCMD via detta API. För det andra lägger PDFium Component till varje ny OCMD i katalogens /OCProperties /OCGs-array, medan /ON, /OFF och /Order förblir gruppenbart. ISO 32000-1 §8.11.4.2 beskriver /OCGs som listan av grupper, men utan den registreringen räknar TPdf.InspectOptionalContent inte alls upp medlemskapet. Bieffekten är att en inventarie av utdata listar varje OCMD som en extra namnlös post i Inventory.Groups, så matcha grupper efter Name i stället för efter position

Hur lappar komponenten en befintlig /OCProperties?

TPdf.SaveAsOptionalContentCreated bygger antingen /OCProperties från grunden eller utökar den som redan finns i katalogen, och den utökar genom att lägga till, aldrig genom att bygga om. Ett dokument utan lager får en ny ordbok med /OCGs och en standardkonfiguration /D som bär /ON, /OFF och /Order, plus /Locked och /RBGroups när någon grupp efterfrågar dem, som beskrivs i ISO 32000-1 §8.11.4.3

För ett dokument som redan har lager lappas varje målarray genom att de nya referenserna fogas in precis före dess avslutande hakparentes. Det låter petigt, och det är där av en anledning. En /Order-array kan hålla nästlade arrayer och textetiketter som formar lagretträdet en visare visar, och ett tidigare angreppssätt som plattade ut och byggde om arrayen tappade de etiketterna. Insättning i slutet behåller det befintliga trädet intakt och listar bara de nya grupperna efter det. Arrayer som bor i egna indirekta objekt löses upp och lappas på plats

Utöka en befintlig /OCProperties-ordbok med PDFium Component i Delphi: nya optional content-gruppreferenser fogas in precis före den avslutande hakparentesen så att nästlade /Order-arrayer och textetiketter överlever, och en indirekt /D-standardkonfiguration följs och lappas på plats
Att fogga in referenser före den avslutande hakparentesen behåller ett befintligt /Order-träd intakt, vilket en ombyggnad som plattar ut nästlade arrayer och etiketter inte kan garantera

Sedan v3.175.0 får också standardkonfigurationen i sig vara indirekt. Vissa producenter skriver /OCProperties << /OCGs [...] /D 12 0 R >>, och PDFium Component följer nu den referensen och lappar mål-ordboken i stället för att misslyckas med ett felaktig-post-fel. Direkta och indirekta sid-/Resources-ordböcker, inklusive en indirekt /XObject-underordbok, hanteras på samma sätt

Att bevisa att de nya lagren verkligen växlar

Ett lager som finns i /OCProperties men inte styr någonting är lätt att producera och svårt att märka. Under utvecklingen wrappade den första bygget av form-skrivaren ett strömobjekt som redan bar sin egen N 0 obj-rubrik och endobj-trailer inuti en andra rubrik. PDFium kunde inte parsa det fördubblade objektet, och i stället för att rapportera ett fel hoppade det tyst över Do-operatorn. Filen öppnades, lagerpanelen såg rätt ut, och den styrda ritningen dök aldrig upp. Det som blottade det var ett prob som växlade lagret och jämförde renderade pixlar: ingen skillnad, så något var fel

Det är varför metoden verifierar före leverans. Utdata återöppnas genom en strikt inläsning som kontrollerar korsreferenstabellen, och varje begärd grupp läses tillbaka via InspectOptionalContent för att bekräfta sitt namn, sitt initiala läge och sin låsregistrering. Först därefter kopieras utdata till din ström, och filoverloaden skriver en temporär fil och ersätter målet atomärt. Du kan köra samma växelkontroll själv genom att återöppna utdata, hitta gruppen efter namn och spara en kopia med motsatt läge

function SaveImperialPreset(const SourceName, TargetName: string): Boolean;
var
  Pdf: TPdf;
  Inventory: TPdfOptionalContentInventory;
  Changes: TPdfOptionalContentStateChanges;
  Options: TPdfOptionalContentConfigureOptions;
  CfgReport: TPdfOptionalContentConfigureReport;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourceName;
    Pdf.Active := True;
    if not Pdf.InspectOptionalContent(Inventory) then
      raise Exception.Create(Inventory.ErrorMessage);

    for I := 0 to High(Inventory.Groups) do
      if (Inventory.Groups[I].Name = 'Dimensions (mm)') or
         (Inventory.Groups[I].Name = 'Dimensions (in)') then
      begin
        SetLength(Changes, Length(Changes) + 1);
        Changes[High(Changes)].GroupObjectNumber := Inventory.Groups[I].ObjectNumber;
        Changes[High(Changes)].Visible := Inventory.Groups[I].Name = 'Dimensions (in)';
      end;

    Options := TPdfOptionalContentConfigureOptions.Default;
    Result := Pdf.SaveAsOptionalContentConfigured(TargetName, Changes,
      Options, CfgReport);
  finally
    Pdf.Free;
  end;
end;

Rendera sida ett av båda filerna och de blå och röda linjerna ska byta plats. Gör de inte det, granska först gruppens EffectiveVisible-värde i inventarien; det speglar läget PDFium faktiskt kommer att rendera efter att konfigurationsarrayer och användningsregler applicerats

Gränser och fällor att planera kring

TPdf.SaveAsOptionalContentCreated är en avgränsad profil, och de flesta överraskningar kommer från att behandla den som en allmän lagereditor. Begränsningarna nedan tvingas fram i förväg, så ett regelbrytande anrop misslyckas med Report.ErrorMessage satt och skriver ingen utdata:

  • Minst en grupp krävs; grupper, medlemskap och former är kapade vid 4096 poster vardera, gruppnamn måste vara unika och 1 till 255 tecken långa, och varje form accepterar högst 16 MiB innehållsbyte
  • Krypterade dokument avvisas, och ett signerat dokument behöver AllowSignedDocument = True; utdata är en fullständig omskrivning, så befintliga signaturer kommer inte längre att validera
  • Varje Forms-post blir sitt eget Form XObject, så att lägga samma överdrag på 200 sidor lagrar innehållet 200 gånger i stället för en
  • Den nya Do-strömmen fogas till efter befintligt sidinnehåll, så den körs i vilket grafiktillstånd de tidigare strömmarna lämnar efter sig; välformade sidor återställer sitt tillstånd, men en sida med obalanserade q/Q kan flytta ditt överdrag
  • Befintligt sidinnehåll flyttas aldrig in i ett lager; bara ritningen du skickar i Forms styrs

Repetitionspunkten är den att väga för stora dokument. Är överdraget tungt och upprepas på varje sida håller delat-objekt-angreppssättet i återanvändbara sidstämplar byggda på Form XObjects en enda kopia i filen, till priset av att hantera lagerporten själv

FAQ

Kan PDFium Component lägga befintligt sidinnehåll i ett nytt lager?

Nej. TPdf.SaveAsOptionalContentCreated styr bara innehållet du levererar i Forms; den wrappar inte befintliga operatorer i markerat innehåll eller flyttar dem in i ett XObject. För att lägga befintlig grafik i lager, återskapa den som självförsörjande forminnehåll och ta bort den ursprungliga ritningen i ett separat steg

Varför är mina styrda annoteringar fortfarande synliga när lagret är av?

PDFium kontrollerar inte /OC-posten hos annoteringsordböcker på sin renderingsväg, så annoteringsnivåstyrning har ingen effekt i PDFium-baserade visare. Det är skälet PDFium Component fäster lagerinnehåll genom Form XObjects, som PDFium faktiskt styr

Vilka objektnummer ska jag använda när jag växlar de nya lagren senare?

Report.CreatedGroupObjectNumbers håller numren i filen som just skrevs. När du återöppnar den filen och konfigurerar den, ta ObjectNumber från TPdf.InspectOptionalContent och matcha efter Name, vilket förblir korrekt även om ett senare sparande omnumrerar objekt

PDFium Component levererar SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured och InspectOptionalContent som en enda optional content-verktygslåda för Delphi och C++Builder, byggd på PDFium-renderaren. Detaljer och nedladdningar finns på produktsidan för PDFium Component