PDFium Component управляет слоями optional content PDF (OCG) в Delphi через два метода TPdf: InspectOptionalContent перечисляет каждый слой вместе с видимостью, которую PDFium реально отрисует, а SaveAsOptionalContentConfigured пишет проверенную копию, в которой выбранные вами слои включены или выключены. Второй метод заодно нейтрализует правила Usage и /AS, которые иначе тихо откатили бы вашу правку. Оба работают с документом, уже открытым в TPdf, так что второго парсера, синхронизируемого с тем, что показывает вьюер, не появляется
Запрос обычно прилетает из CAD- или GIS-конторы: комплект чертежей уходит с размерами, аннотациями и основной надписью на отдельных слоях, а заказчик хочет копию со скрытыми размерами до отправки поставщику. PDFium отрисовывает optional content корректно, но в его публичном 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 << /View << /ViewState /OFF >> >>, которое переигрывает всё выше - Считаем группу, чей
/Intentне содержит ни/View, ни/All, видимой, поскольку она не участвует в видимости view-intent - Наконец прогоняем массив
/ASвыбранной конфигурации, чьи записи для совпавшего события выставляют состояние перечисленных в них групп
Третий шаг — тот, что обжигает людей. Файл, сохранённый вёрстальным инструментом, часто несёт /ViewState /ON на каждом OCG, и тогда PDFium игнорирует ваш тщательно выправленный массив /OFF: сохранение проходит, файл чисто переоткрывается, а слой всё равно рисуется. Для Print и Export OcExplicitUsageState сначала читает PrintState или ExportState и откатывается к ViewState, когда конкретной записи нет, так что одиночный ViewState /ON пришпиливает слой и для печати. Marked content, ссылающийся на OCMD (§8.11.2.2), затем разрешается против этих погрупповых результатов — через политику /P или, когда есть, выражение видимости /VE
Как перечислить слои, которые PDFium реально покажет?
TPdf.InspectOptionalContent возвращает TPdfOptionalContentInventory, чей массив Groups несёт для каждого OCG номер объекта, имя, intents, три Usage-состояния, язык, диапазон zoom, флаг Locked, индекс radio-группы и вычисленную EffectiveVisible. Метод сначала заставляет PDFium сохранить текущий документ в памяти, разворачивает object streams и сканирует результат, так что правки, сделанные ранее в сессии, отражаются. Конфигурация с индексом 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 выбранной конфигурации удаляется, чтобы событие открытия, печати или экспорта не могло вернуть слои назад
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 PDF в PDFium Component
От чего отказывается сконфигурированное сохранение?
Сконфигурированное сохранение отказывает любой правке, которую запрещает сам документ или которую он не может безопасно представить, и каждый отказ случается до того, как тронут приёмник. Номер объекта, которого нет в /OCGs, проваливается сразу. Смена группы, перечисленной в массиве /Locked конфигурации, проваливается, хотя повторение её текущего значения разрешено. Со включённым EnforceRadioGroups любой набор /RBGroups, который кончил бы больше чем одной видимой участницей, отвергается вместо тихого выключения остальных. Зашифрованные документы отвергаются, потому что открытые инкрементальные объекты не могут нести активный security handler. Подписанные документы бросают 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;
Поймите компромиссы, прежде чем вшивать это в батч-задачу. Дописанная ревизия сидит поверх полного re-save PDFium, а не поверх байтов вашего исходного файла, — ровно поэтому подписанный вход требует явного согласия. Переписывание заодно нормализует выбранную конфигурацию к /BaseState /ON, так что базлайн автора /Unchanged или /OFF заменяется явными массивами с той же итоговой видимостью. Выбрасывание /AS убирает приёмы только для печати, вроде слоя-водяного знака, появляющегося лишь на бумаге; поставьте DisableAutomaticState в False, чтобы сохранить эти правила, приняв, что они могут переиграть запрошенное вами состояние для того события. Плюс на другой чаше: PDF/A-2 (ISO 19005-2 clause 6.9) и PDF/UA (ISO 14289-1 clause 7.10) оба запрещают /AS в словарях конфигураций, так что дефолтный вывод снимает одну претензию, которую иначе показала бы PDF/A preflight-валидация с PDFium Component
Где контроль слоёв встаёт в Delphi-вьюере PDF
Во вьюере контроль слоёв — это чеклист, питаемый инвентарём, плюс перезагрузка сохранённого результата. Заполните чеклист из Groups, задизейбльте записи с Locked, считайте участников с общим RadioGroupIndex взаимоисключающими, а по кнопке применить пишите в TMemoryStream и грузите этот поток обратно в TPdf, чтобы вьюер отрисовал новое состояние. Разводка между TPdf и TPdfView разобрана в статье про сборку насыщенного PDF-вьюера с PDFium VCL в Delphi. Лицензирование, пробные загрузки и остальной набор фич — на странице продукта PDFium Component для Delphi