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
/OCve vlastním slovníku, kontrolované, když se XObject maluje - Anotace s položkou
/OCve 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
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
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ě
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
Formsse stane vlastním Form XObject, takže stejný překryv na 200 stránek uloží obsah 200krát místo jednou - Nový stream
Dose 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ýmiq/Qmůž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