Technický článek

Tvorba PDF vrstev (OCG a OCMD) v Delphi s PDFium

PDFium Component vytváří nové PDF vrstvy z Delphi přes TPdf.SaveAsOptionalContentCreated. V jednom ověřeném uložení metoda přidá skupiny optional content (OCG), slovníky členství optional content (OCMD) a kresbu dodanou volajícím zabalenou ve Form XObjects, jejichž položka /OC míří na novou skupinu, takže PDFium i další prohlížeče ukáží nebo skryjí tu kresbu, když se vrstva přepne

Požadavek bývá zřídka abstraktní. Inženýrský tým v závodě chce metrické i imperiální rozměry na témže výkresu, s viditelnou právě jednou sadou. Tiskárna chce překryv řezné linie, který se ukazuje na obrazovce, ale dá se vypnout před korrekturou. Ani jeden soubor zatím žádné vrstvy nemá, takže není co přepínat. Přepínání existujících vrstev je jiná práce, popsaná v přepínání existujících vrstev optional content PDF přes SaveAsOptionalContentConfigured; tenhle článek je o tom vrstvy vůbec vytvořit

Proč nemůžete jen nastavit /OC na page objektu s PDFium?

PDFium nemá žádnou veřejnou funkci pro připojení optional content k existujícímu page objektu. Veřejné hlavičky neobsahují FPDFPageObj_SetOC ani nic ekvivalentního, takže obvyklá cesta edituj-pak-FPDFPage_GenerateContent nemůže produkovat braný obsah. Brána se musí zapsat do struktury souboru a jedinou otázkou je, kterou strukturu PDFium při renderování poctí

ISO 32000-1 §8.11.3 dává tři způsoby, jak udělat grafiku volitelnou, a PDFium je obsluhuje odlišně:

  • Sekce content streamu stránky zabalené do marked content /OC /Name BDC ... EMC, kontrolované po page objektech během renderování
  • Form XObjects a image XObjects nesoucí položku /OC ve vlastním slovníku, kontrolované, když se XObject maluje
  • Anotace s položkou /OC ve slovníku anotace, které PDFium rendering cesta vůbec nekontroluje, takže braná anotace zůstává viditelná bez ohledu na stav vrstvy

Obalení marked contentem znamená přepisování existujících content streamů operátor po operátoru a anotace vylučuje renderer. Zůstává cesta Form XObject a to je ta, kterou PDFium Component používá: vaše kresba jde do nového Form XObject s /OC mířícím na skupinu, form se registruje ve slovníku /Resources /XObject stránky a nový content stream držící jediné /Name Do se připojuje k /Contents stránky. Existující obsah stránky se nikdy neparsuje ani nepřepisuje

Cesty braní optional content ISO 32000-1 pod PDFium: marked content mezi OC BDC a EMC se kontroluje po page objektech, Form XObject s položkou /OC se kontroluje při malování a je to cesta, kterou PDFium Component zapisuje z Delphi, zatímco anotační /OC se nikdy nekontroluje
PDFium kontroluje optional content pro sekce marked content a slovníky XObject, ale ne pro anotace, proto braná SaveAsOptionalContentCreated kresba jde přes Form XObject a operátor Do

Popis skupin a braných formů v Delphi

TPdf.SaveAsOptionalContentCreated bere tři pole a vrací report. Oba overloady, jeden zapisující do TStream a jeden do názvu souboru, sdílejí tentýž seznam parametrů: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport a volitelné AllowSignedDocument: Boolean = False. Metoda přibyla v PDFium Component v3.154.0

Každý TPdfOptionalContentCreateGroup má Name, volitelný Creator, Intent pociView nebo pociDesign, příznak InitialStateVisible, příznak Locked a RadioTag. Skupiny sdílející tentýž RadioTag dopadnou do jediného pole /RBGroups, což je to, jak prohlížeč pozná, že zapnutí jedné vypne ostatní. Každý TPdfOptionalContentCreateForm jmenuje svou branou skupinu přes GroupIndex (nulový index do vašeho pole Groups), jednoho základu PageNumber, syrové bajty content streamu, bounding box, volitelnou matici a volitelný 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 vyplní záznamy nulami
    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;

Všimněte si, že obsah formu je čistě vektorová kresba. Komponenta na každém formu píše jen prázdný slovník /Resources, takže content stream musí být soběstačný: device barvy, cesty, ořez a transformace fungují, ale cokoli, co potřebuje pojmenovaný zdroj (font pro Tf, obrázek pro Do, ExtGState pro gs), nemá proti čemu se rozlišit. Necháte-li ResourceName prázdný, formy se pojmenují FmOc1, FmOc2 a tak dále; vlastní název smí obsahovat jen písmena, číslice, podtržítko, minus a tečku

Co přidává OCMD nad prostou skupinou?

Slovník členství optional content udělá viditelnost závislou na několika skupinách najednou. ISO 32000-1 §8.11.2.2 definuje dva tvary: seznam /OCs s politikou /P jako /AnyOn nebo /AllOn a výraz viditelnosti /VE stavěný ze specifikace z /And, /Or a /Not. PDFium Component bere TPdfOptionalContentCreateMembership s MemberGroupIndices, Policy a Expression; není-li Expression.GroupIndices prázdné, vyhrává výraz a /VE se zapisuje vedle /OCs

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

  // Viditelné, když je zapnutá kterákoli unit vrstva: /OCs [...] /P /AnyOn
  SetLength(Memberships[0].MemberGroupIndices, 2);
  Memberships[0].MemberGroupIndices[0] := 0;
  Memberships[0].MemberGroupIndices[1] := 1;
  Memberships[0].Policy := ocmpAnyOn;

  // Viditelné jen dokud je imperiální vrstva vypnutá: /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;

Podporovaná podmnožina je záměrně úzká. Politiky jsou omezené na ocmpAllOn a ocmpAnyOn; ocmpAnyOff a ocmpAllOff se odmítají. Výrazy jsou hluboké jednu úroveň, s poceAll, poceAny nebo poceNot nad vytvořenými skupinami a /Not vyžaduje přesně jeden index skupiny. poceAll se serializuje jako /And, poceAny jako /Or a poceNot jako /Not, jediné tři operátory, které §8.11.2.2 definuje. Verze před 3.181.2 zapisovaly /All a /Any, které PDFium vyhodnocuje jako skryté, takže jste-li OCMD vytvořili starším buildem, vygenerujte je znovu místo důvěry ve výraz. Každé členství taky potřebuje aspoň jednu položku v MemberGroupIndices, i když je výraz přítomný, protože ten seznam se stává polem /OCs. Čísla objektů nových slovníků se vrátí v Report.CreatedMembershipObjectNumbers

Slovníky členství optional content vytvořené PDFium Component v Delphi: seznam /OCs s politikou /AnyOn nebo /AllOn vyhodnocuje stavy skupin společně, zatímco výraz viditelnosti /VE jako /Not na skupině braná obsah logikou And, Or a Not hlubokou jednu úroveň
Politiky vyhodnocují seznam /OCs, zatímco výrazy /VE kombinují skupiny přes /And, /Or a /Not - tak jako tak potřebuje členství aspoň jednu členskou skupinu k odkazu

Dvě chování stojí za znalost, než na OCMD postavíte. Za prvé, brané formy ve stejném volání odkazují jen skupiny: TPdfOptionalContentCreateForm.GroupIndex indexuje Groups, ne Memberships, takže form nemůže být touto API braný nově vytvořeným OCMD. Za druhé, PDFium Component přidává každý nový OCMD do pole katalogu /OCProperties /OCGs, zatímco /ON, /OFF a /Order zůstávají jen pro skupiny. ISO 32000-1 §8.11.4.2 popisuje /OCGs jako seznam skupin, ale bez té registrace TPdf.InspectOptionalContent členství vůbec nevyčísluje. Vedlejším efektem je, že inventář výstupu vypisuje každý OCMD jako extra nepojmenovanou položku v Inventory.Groups, takže párujte skupiny podle Name, ne podle pozice

Jak komponenta záplatuje existující /OCProperties?

TPdf.SaveAsOptionalContentCreated buď postaví /OCProperties od nuly, nebo rozšíří ten už přítomný v katalogu, a rozšiřuje přidáváním, nikdy přestavbou. Dokument bez vrstev dostane nový slovník s /OCGs a defaultní konfigurací /D nesoucí /ON, /OFF a /Order, plus /Locked a /RBGroups, když si je některá skupina vyžádá, jak rozkládá ISO 32000-1 §8.11.4.3

U dokumentu, který už vrstvy má, se každé cílové pole záplatuje vložením nových referencí těsně před jeho uzavírací závorku. To zní vybíravě a je to tam z důvodu. Pole /Order může držet vnořená pole a textové popisky, které tvarují strom vrstev, který prohlížeč ukazuje, a dřívější přístup, který pole zploštil a přestavěl, ty popisky ztrácel. Vložení na konec drží existující strom v celku a prostě vypíše nové skupiny za ním. Pole žijící ve vlastních indirect objektech se vyřeší a záplatují na místě

Rozšíření existujícího slovníku /OCProperties PDFium Component v Delphi: nové reference skupin optional content se vloží těsně před uzavírací závorku, takže vnořená pole /Order a textové popisky přežijí, a indirect defaultní konfigurace /D se sleduje a záplatuje na místě
Přidání referencí před uzavírací závorku drží existující strom /Order v celku, co přestavba zplošťující vnořená pole a popisky nemůže garantovat

Od v3.175.0 může být i samotná defaultní konfigurace indirect. Někteří producenti píší /OCProperties << /OCGs [...] /D 12 0 R >> a PDFium Component teď tu referenci sleduje a záplatuje cílový slovník místo selhání s chybou poškozené položky. Přímé i indirect slovníky /Resources stránek, včetně indirect sub-slovníku /XObject, se obsluhují stejně

Dokázání, že nové vrstvy skutečně přepínají

Vrstva, která existuje v /OCProperties, ale nic nebraní, se snadno vytvoří a těžko postřehne. Během vývoje první stavba form writeru zabalila stream objekt, který už nesl vlastní hlavičku N 0 obj a trailer endobj, do druhé hlavičky. PDFium nedokázalo zdvojený objekt parsovat a místo nahlášení chyby tiše přeskočilo operátor Do. Soubor se otevřel, panel vrstev vypadal správně a braná kresba se nikdy neukázala. Odhalila to sonda, která přepnula vrstvu a srovnala vyrenderované pixely: žádný rozdíl, takže něco bylo špatně

Proto metoda ověřuje před dodáním. Výstup se znovu otevře přes přísné načtení kontrolující cross-reference tabulku a každá požadovaná skupina se přečte zpět přes InspectOptionalContent, aby se potvrdilo její jméno, její počáteční stav a její registrovaný zámek. Teprve pak se výstup zkopíruje do vašeho streamu a souborový overload zapisuje dočasný soubor a vyměňuje cíl atomicky. Totéž přepínání si můžete pustit sami znovuotevřením výstupu, nalezením skupiny podle jména a uložením kopie s opačným stavem

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;

Vykreslete stránku jedna obou souborů a modrá a červená linie by se měly vyměnit. Nestane-li se tak, prozkoumejte nejdřív hodnotu EffectiveVisible skupiny v inventáři; odráží stav, který PDFium po aplikaci konfiguračních polí a pravidel užití skutečně vyrenderuje

Limity a pasti, kolem kterých plánovat

TPdf.SaveAsOptionalContentCreated je ohraničený profil a většina překvapení přichází z brání ho jako obecný editor vrstev. Omezení níže se vynucují předem, takže porušující volání selže s nastaveným Report.ErrorMessage a nezapíše žádný výstup:

  • Je vyžadována aspoň jedna skupina; skupiny, členství i formy mají strop 4096 položek každý, názvy skupin musí být unikátní a dlouhé 1 až 255 znaků a každý form přijme nejvýš 16 MiB bajtů obsahu
  • Šifrované dokumenty se odmítají a podepsaný dokument potřebuje AllowSignedDocument = True; výstup je kompletní přepis, takže existující podpisy už nebudou validovat
  • Každá položka Forms se stane vlastním Form XObject, takže stejný překryv na 200 stránek uloží obsah 200krát místo jednou
  • Nový stream Do se připojuje za existujícím obsahem stránky, takže běží v jakémkoli grafickém stavu, který dřívější streamy zanechají; dobře utvářené stránky svůj stav obnoví, ale stránka s nevyváženými q/Q může váš překryv posunout
  • Existující obsah stránky se nikdy nepřesouvá do vrstvy; braná je jen kresba, kterou předáte v Forms

Bodem opakování je to, co vážit u velkých dokumentů. Je-li překryv těžký a opakuje se na každé stránce, přístup sdíleného objektu v znovupoužitelných page stampách stavěných na Form XObjects drží v souboru jedinou kopii, za cenu obsluhy brány vrstvy vlastními silami

FAQ

Může PDFium Component dát existující obsah stránky do nové vrstvy?

Ne. TPdf.SaveAsOptionalContentCreated braná jen obsah, který dodáte v Forms; nezabaluje existující operátory do marked content ani je nepřesouvá do XObject. Pro vrstvení existující grafiky ji vygenerujte znovu jako soběstačný obsah formu a původní kresbu odstraňte v samostatném kroku

Proč jsou moje brané anotace stále viditelné, když je vrstva vypnutá?

PDFium na své renderovací cestě nekontroluje položku /OC slovníků anotací, takže braní na úrovni anotací nemá v prohlížečích založených na PDFium žádný efekt. Proto připojuje PDFium Component vrstvový obsah přes Form XObjects, které PDFium braná

Která čísla objektů mám použít při pozdějším přepínání nových vrstev?

Report.CreatedGroupObjectNumbers drží čísla v souboru, který se právě zapsal. Když ten soubor znovu otevřete a konfigurujete, berte ObjectNumber z TPdf.InspectOptionalContent a párujte podle Name, což zůstává správné, i když pozdější uložení objekty přečísluje

PDFium Component dodává SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured a InspectOptionalContent jako jeden toolkit optional content pro Delphi a C++Builder, postavený na rendereru PDFium. Detaily a stažení jsou na stránce produktu PDFium Component