PDFium Component управлява PDF optional content слоеве (OCG) в Delphi чрез два метода на TPdf: InspectOptionalContent изброява всеки слой заедно с видимостта, която PDFium реално ще рендерира, а SaveAsOptionalContentConfigured записва верифицирано копие, в което слоевете, които изберете, са изключени или включени. Вторият метод също неутрализира Usage и /AS правилата, които иначе тихо биха отменили редакцията ви. И двата работят върху документа, вече отворен в TPdf, така че няма втори parser за синхронизиране с това, което viewer-ът показва
Заявката обикновено идва от CAD или GIS работилница: чертожният комплект пътува с размери, анотации и титулно табло на отделни слоеве, а клиентът иска копие със скрити размери, преди то да отиде при доставчик. PDFium рендерира optional content коректно, но публичният му ABI няма функция да изброява OCG-и, да избира конфигурация или да обръща състоянието на слой. Така че слиза до ниво обекти, редактирате /OCProperties, записвате, презареждате — а слоят си е там. Причината е visibility логиката на PDFium и си струва да я разберете, преди да пипнете хоть байт
Защо редакция на /ON и /OFF не променя това, което PDFium рендерира?
Редакцията на масивите /ON и /OFF на конфигурационния речник не стига, защото PDFium позволява изрично състояние вътре в собствения /Usage речник на OCG-я да надделее над тези масиви, а /AS auto-state правило после може да надвие и двете. ISO 32000-1 §8.11.4 описва конфигурациите и usage речниците като отделни механизми; renderer-ът на PDFium сгъва и двете в едно решение, а InspectOptionalContent го възпроизвежда в този ред:
- Тръгнете от
/BaseStateна конфигурацията, където/ONи/Unchangedи двете броят като видими и само/OFFкрие - Приложете масива
/ONна конфигурацията, после нейния/OFF, така че група, попаднала и в двата, свършва скрита - Приложете изричното 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 закова слоя и за печат. Marked content, рефериращ OCMD (§8.11.2.2), после се resolve-ва срещу тези резултати по група, чрез политиката /P или, когато присъства, visibility израза /VE
Как изброявате слоевете, които PDFium реално ще покаже?
TPdf.InspectOptionalContent връща TPdfOptionalContentInventory, чийто масив Groups носи object номера, името, intents, трите Usage състояния, езика, zoom диапазона, флага Locked, radio-group индекса и изчислената EffectiveVisible на всеки OCG. Методът първо кара PDFium да запише текущия in-memory документ, разширява object stream-овете и сканира резултата, така че редакциите от по-рано в сесията се отразяват. Configuration индекс 0 е винаги default речникът /D, а записите на /Configs следват от индекс 1; default аргументът -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. Няколко edge правила са нарочни. /P по подразбиране е /AnyOn, а OCMD без групи брои като видим. Референция към object номер, който не е познат OCG, се третира като видима, вместо да провали целия израз. Оценката на /VE спира на дълбочина на влагане 32 и третира всичко по-дълбоко като скрито, което пази враждебен или саморефериращ се израз да не превърне инспекцията в stack overflow
Запис на ново състояние на слой със SaveAsOptionalContentConfigured
TPdf.SaveAsOptionalContentConfigured приема масив от record-и TPdfOptionalContentStateChange (object номер на група плюс 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 като байт-по-байт префикс и долепя само пренаписания configuration притежател и OCG обектите, носещи Usage речници, следвани от нова xref секция и trailer. Преди хоть един байт да стигне вашата дестинация, резултатът се отваря наново в отделен TPdf под строгата load политика, а методът се проваля, ако cross-reference таблицата не се валидира. File overload-ът отива стъпка по-напред: пише във временен файл до целта и заменя целта само след успешна верификация, така че отхвърлена актуализация никога не оставя полузаписан чертеж след себе си. Това е същият верифициран incremental revision подход, използван от редактора на PDF name tree и number tree в PDFium Component
Какво отказва да направи configured записът?
Configured записът отказва всяка промяна, която самият документ забранява или не може да представи безопасно, а всяко отказване става преди дестинацията да е докосната. Object номер, който не е в /OCGs, се проваля тотално. Промяна на група, изброена в масива /Locked на конфигурацията, се проваля, макар да се допуска потвърждаване на текущата ѝ стойност. С включен EnforceRadioGroups всяко множество /RBGroups, което би свършило с повече от един видим член, се отхвърля, вместо тихо да изключи останалите. Шифровани документи се отхвърлят, защото plaintext incremental обекти не могат да носят активния security handler. Подписани документи вдигат EPdfError, освен ако не подадете AllowSignedDocument = True, тъй като промяната на това, което страница показва, може да счупи signature coverage или certification политика
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;
Опознайте компромисите, преди да включите това в batch job. Долепената revision седи върху пълния re-save на PDFium, не върху оригиналните ви файлови байтове, което е точно причината подписан вход да иска изрично съгласие. Пренаписването също нормализира избраната конфигурация към /BaseState /ON, така че baseline /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 в конфигурационни речници, така че default изходът маха един проблем, който PDF/A preflight валидацията с PDFium Component иначе би докладвала
Къде пасва управлението на слоеве в Delphi PDF viewer
В viewer управлението на слоеве е чеклист, задвижван от инвентара плюс презареждане на записания резултат. Напълнете чеклиста от Groups, изключете записите, които са Locked, третирайте членове, споделящи RadioGroupIndex, като взаимно изключващи се, и при прилагане пишете в TMemoryStream и заредете този поток обратно в TPdf, така че изгледът да нарисува новото състояние. Свързването между TPdf и TPdfView е разгледано в статията за изграждане на богат на функции PDF viewer с PDFium VCL в Delphi. Лицензиране, trial изтегляния и останалата част от функциите са на продуктовата страница на PDFium Component за Delphi