Техническа статия

Превключване на optional content слоеве в Delphi с PDFium

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 на избраната конфигурация, чиито записи за съответстващото събитие задават състоянието на изброените от тях групи
Петстъпковото visibility решение, което PDFium Component възпроизвежда за всяка PDF optional content група в Delphi: BaseState задава началото, масивите ON и OFF на конфигурацията се прилагат по ред, изричен Usage запис ViewState или PrintState надвива и двете, неучастието по Intent брои като видимо, а масивът AS върви последен
Редакция на масивите ON и OFF не стига, защото PDFium сгъва BaseState, двата масива, Usage състоянието на групата и накрая AS auto-state правилата в една присъда, която InspectOptionalContent възпроизвежда стъпка по стъпка

Третата стъпка е тази, която изгаря хората. Файл, записан от 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

Как SaveAsOptionalContentConfigured в PDFium Component записва Delphi PDF със сменени слоеве: промени в състоянието и опции влизат, избраната конфигурация се пренаписва с пълни масиви ON и OFF и Usage състояния, верифицираната incremental revision се долепя, а строгото повторно отваряне трябва да валидира, преди каквото и да е записано
Configured записът пази собствения re-save на PDFium като байтов префикс, долепя пренаписания configuration притежател плюс нова xref секция и отваря резултата наново в отделен TPdf, преди дестинацията да е докосната

Какво отказва да направи configured записът?

Configured записът отказва всяка промяна, която самият документ забранява или не може да представи безопасно, а всяко отказване става преди дестинацията да е докосната. Object номер, който не е в /OCGs, се проваля тотално. Промяна на група, изброена в масива /Locked на конфигурацията, се проваля, макар да се допуска потвърждаване на текущата ѝ стойност. С включен EnforceRadioGroups всяко множество /RBGroups, което би свършило с повече от един видим член, се отхвърля, вместо тихо да изключи останалите. Шифровани документи се отхвърлят, защото plaintext incremental обекти не могат да носят активния security handler. Подписани документи вдигат EPdfError, освен ако не подадете AllowSignedDocument = True, тъй като промяната на това, което страница показва, може да счупи signature coverage или certification политика

Отказващите гейтове, които SaveAsOptionalContentConfigured прилага в PDFium Component преди да запише configured Delphi PDF: object номер извън OCG-и се проваля, заключени групи се провалят, RBGroups множества с повече от един видим член се отхвърлят, шифровани документи не могат да носят plaintext incremental обекти, а подписани файлове искат AllowSignedDocument
Всяко отказване става преди дестинацията да е докосната, а причината за провала каца в Report.ErrorMessage, вместо да остави полузаписан чертеж след себе си
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