PDFium Component керує шарами необов'язкового вмісту PDF (OCG) у Delphi через два методи TPdf: InspectOptionalContent перелічує кожен шар разом із видимістю, яку PDFium справді відрендерить, а SaveAsOptionalContentConfigured пише перевірену копію, у якій обрані вами шари увімкнено чи вимкнено. Другий метод заодно нейтралізує правила Usage і /AS, які інакше мовчки скасували б вашу правку. Обидва працюють з документом, уже відкритим у TPdf, тож немає другого парсера, який треба синхронізувати з тим, що показує глядач
Запит зазвичай приходить від CAD чи GIS контори: комплект креслень їде з розмірами, анотаціями і титульним блоком на окремих шарах, а клієнт хоче копію з прихованими розмірами, перш ніж вона поїде до постачальника. PDFium рендерить необов'язковий вміст коректно, але його публічний ABI не має функції, щоб перелічити OCG, обрати конфігурацію чи перевернути стан шару. Тож ви спускаєтеся на рівень об'єктів, редагуєте /OCProperties, зберігаєте, перезавантажуєте — а шар все ще там. Причина в логіці видимості PDFium, і варто її зрозуміти, перш ніж торкатися хоч одного байта
Чому правка /ON і /OFF не міняє те, що рендерить PDFium?
Редагування масивів /ON і /OFF у словнику конфігурації — недостатньо, бо PDFium дозволяє явному стану всередині власного словника /Usage OCG перемагати ті масиви, а правило авто-стану /AS може тоді перекрити обидва. ISO 32000-1 §8.11.4 описує конфігурації та словники usage як окремі механізми; рендерер PDFium згортає їх в одне рішення, і InspectOptionalContent відтворює його в такому порядку:
- Стартуйте з
/BaseStateконфігурації, де/ONі/Unchangedобидва рахуються видимими, і лише/OFFховає - Застосуйте масив
/ONконфігурації, потім її масив/OFF, тож група, яка є в обох, опиняється схованою - Застосуйте явний Usage-стан групи для проханого usage, як-от
/Usage << /View << /ViewState /OFF >> >>, який перекриває все вище - Трактуйте групу, в чиєму
/Intentнемає ані/View, ані/All, як видиму, бо вона не бере участі у видимості view-intent - Нарешті запустіть масив
/ASобраної конфігурації, чиї записи для відповідної події виставляють стан перелічених груп
Третій крок — той, що палить людей. Файл, збережений layout-інструментом, часто несе /ViewState /ON на кожному OCG, і PDFium тоді ігнорує ваш старанно відредагований масив /OFF: збереження проходить, файл відкривається чисто, а шар все одно малюється. Для Print і Export OcExplicitUsageState читає спершу PrintState чи ExportState і падає назад на ViewState, коли специфічного запису немає, тож самотній ViewState /ON пришпилює шар і для друку. Позначений вміст, що посилається на OCMD (§8.11.2.2), потім розв'язується проти цих по-групових результатів — через політику /P чи, за наявності, вираз видимості /VE
Як перелічити шари, які PDFium справді покаже?
TPdf.InspectOptionalContent повертає TPdfOptionalContentInventory, масив Groups якого несе для кожного OCG номер об'єкта, ім'я, intents, три Usage-стани, мову, діапазон zoom, прапорець Locked, індекс radio-group і обчислену EffectiveVisible. Метод спершу змушує PDFium зберегти поточний документ у пам'яті, розгортає об'єктні потоки і сканує результат, тож правки, зроблені раніше в сесії, відображені. Індекс конфігурації 0 — це завжди типовий словник /D, а записи /Configs йдуть з індексу 1; типовий аргумент -1 обирає індекс 0. Документ без /OCProperties змушує метод повернути False з причиною в ErrorMessage, не піднімаючи винятку
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
G: TPdfOptionalContentGroup;
begin
// Usage типово ocuView; -1 обирає конфігурацію 0, словник /D
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;
Масив Memberships звітує кожен OCMD з його Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), сирим текстом VisibilityExpression і власною EffectiveVisible. Кілька крайових правил навмисні. /P типово /AnyOn, і OCMD без груп рахується видимим. Посилання на номер об'єкта, який не є відомим OCG, трактується як видиме, а не фейлить увесь вираз. Обчислення /VE зупиняється на глибині вкладення 32 і трактує все глибше як сховане, що не дає ворожому чи самопосилальному виразу перетворити інспекцію на stack overflow
Запис нового стану шару через SaveAsOptionalContentConfigured
TPdf.SaveAsOptionalContentConfigured бере масив записів TPdfOptionalContentStateChange (номер об'єкта групи плюс Visible) і пише документ, у якому обрана конфігурація дає рівно цей стан. Обрана конфігурація отримує /BaseState /ON плюс повні масиви /ON і /OFF, що покривають кожну групу, а кожен OCG, у якого вже є словник Usage, отримує явний ViewState (чи PrintState / ExportState, за Options.Usage), що відповідає його новому стану. З TPdfOptionalContentConfigureOptions.Default ключ /AS обраної конфігурації вилучається, щоб подія open, print чи export не могла перевернути шари назад
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;
// Конфігурація 0, ocuView, DisableAutomaticState і 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;
Шлях запису тримає власний збережений вихід PDFium як побайтовий префікс і додає лише переписаного власника конфігурації та об'єкти OCG, які несуть словники Usage, за ними нову секцію xref і trailer. Перш ніж хоч один байт дістане вашої цілі, результат заново відкривається в окремому TPdf під строгою політикою завантаження, і метод фейлить, якщо таблиця перехресних посилань не валідується. Перевантаження з файлом іде на крок далі: воно пише в тимчасовий файл поруч із ціллю і замінює ціль лише після успішної верифікації, тож відхилена правка ніколи не лишає за собою напівзаписане креслення. Це той самий підхід верифікованої інкрементальної ревізії, який використовує редактор name tree і number tree у PDFium Component
Чого налаштоване збереження відмовляється робити?
Налаштоване збереження відмовляє будь-яку зміну, яку сам документ забороняє чи не може безпечно представити, і кожна відмова відбувається до того, як цілю буде торкнуто. Номер об'єкта, якого немає в /OCGs, фейлить одразу. Зміна групи, переліченої в масиві /Locked конфігурації, фейлить, хоча переписування її поточного значення дозволене. З увімкненим EnforceRadioGroups будь-який набір /RBGroups, який закінчив би більш ніж одним видимим членом, відхиляється замість мовчазного вимкнення інших. Зашифровані документи відхиляються, бо інкрементальні об'єкти у відкритому тексті не можуть нести активний обробник безпеки. Підписані документи піднімають EPdfError, якщо ви не передасте AllowSignedDocument = True, бо зміна того, що показує сторінка, може зламати покриття підпису чи політику сертифікації
function TFormMain.SavePrintPreset(Target: TStream;
const Changes: TPdfOptionalContentStateChanges): Boolean;
var
Options: TPdfOptionalContentConfigureOptions;
Report: TPdfOptionalContentConfigureReport;
begin
Options := TPdfOptionalContentConfigureOptions.Default;
Options.Usage := ocuPrint; // пише /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // перший запис /Configs, не /D
try
Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
Report); // AllowSignedDocument лишається False
if not Result then
ShowMessage(Report.ErrorMessage);
except
on E: EPdfError do
begin
ShowMessage(E.Message); // підписаний файл: у Target не записано нічого
Result := False;
end;
end;
end;
Знайте trade-off-и, перш ніж вмонтовувати це в пакетну роботу. Додана ревізія сидить поверх повного re-save PDFium, а не ваших початкових байтів файлу, — саме тому підписаний вхід потребує явної згоди. Переписування також нормалізує обрану конфігурацію до /BaseState /ON, тож базова лінія автора /Unchanged чи /OFF замінюється явними масивами з тією самою результуючою видимістю. Викидання /AS прибирає print-only трюки на кшталт шару водяного знака, що з'являється лише на папері; виставте DisableAutomaticState у False, щоб ті правила лишилися, приймаючи, що вони можуть перекрити ваш проханий стан для тієї події. З плюсів: PDF/A-2 (ISO 19005-2 clause 6.9) і PDF/UA (ISO 14289-1 clause 7.10) обидва забороняють /AS у словниках конфігурацій, тож типовий вихід прибирає одну проблему, яку інакше прозвітувала б ваша preflight-валідація PDF/A з PDFium Component
Де керування шарами стає до ладу в PDF-глядачі на Delphi
У глядачі керування шарами — це чеклист, що живиться з інвентаря плюс перезавантаженням збереженого результату. Наповніть чеклист із Groups, вимкніть записи, що Locked, трактуйте членів, які ділять RadioGroupIndex, як взаємовиключні, і при застосуванні пишіть у TMemoryStream і завантажуйте той потік назад у TPdf, щоб вид намалював новий стан. Проводку між TPdf і TPdfView покриває стаття про побудову багатофункціонального PDF-глядача на PDFium VCL у Delphi. Ліцензування, trial-завантаження і решта набору функцій — на сторінці продукту PDFium Component for Delphi