기술 문서

Delphi에서 PDFium으로 PDF 선택 콘텐츠 레이어 토글하기

PDFium Component는 두 개의 TPdf 메서드로 Delphi에서 PDF 선택 콘텐츠 레이어(OCG)를 다룹니다. InspectOptionalContent는 PDFium이 실제로 렌더할 가시성과 함께 모든 레이어를 나열하고, SaveAsOptionalContentConfigured는 여러분이 고른 레이어가 켜지거나 꺼진 검증된 사본을 씁니다. 두 번째 메서드는 그렇지 않으면 편집을 조용히 되돌릴 Usage와 /AS 규칙도 무력화합니다. 둘 다 TPdf에 이미 열려 있는 문서로 동작하므로, 뷰어가 보여 주는 것과 맞춰야 할 두 번째 파서는 없습니다

요청은 보통 CAD나 GIS 업체에서 옵니다. 도면 세트는 치수, 주석, 제목 블록이 각각 다른 레이어에 실려 나가고, 고객은 공급자에게 보내기 전에 치수가 숨겨진 사본을 원합니다. PDFium은 선택 콘텐츠를 올바르게 렌더하지만, 공개 ABI에는 OCG를 열거하거나 구성을 고르거나 레이어 상태를 뒤집는 함수가 없습니다. 그래서 객체 수준으로 내려가 /OCProperties를 편집하고 저장하고 다시 로드해도 레이어는 그대로 있습니다. 원인은 PDFium의 가시성 로직이고, 바이트를 건드리기 전에 이해해 둘 가치가 있습니다

/ON과 /OFF를 편집해도 PDFium이 렌더하는 게 왜 안 바뀔까요?

구성 딕셔너리의 /ON과 /OFF 배열을 편집하는 것만으로는 부족합니다. PDFium은 OCG 자체의 /Usage 딕셔너리 안에 든 명시적 상태가 그 배열들을 이기게 두고, /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 배열을 돌립니다. 일치하는 이벤트에 대한 항목이 자기 목록의 그룹 상태를 정합니다
PDFium Component가 Delphi에서 모든 PDF 선택 콘텐츠 그룹에 대해 재현하는 5단계 가시성 결정: BaseState가 출발점을 정하고, 구성의 ON과 OFF 배열이 순서대로 적용되며, 명시적 Usage ViewState나 PrintState 항목이 둘 다를 덮어쓰고, Intent 불참은 보이는 것으로 치며, AS 배열이 마지막에 돕니다
ON과 OFF 배열 편집만으로는 부족합니다. PDFium은 BaseState와 두 배열, 그룹 Usage 상태, 마지막으로 AS 자동 상태 규칙을 하나의 판정으로 접어 넣고, InspectOptionalContent가 그것을 단계별로 재현합니다

사람을 태우는 건 세 번째 단계입니다. 레이아웃 도구가 저장한 파일은 종종 모든 OCG에 /ViewState /ON을 지니고, 그러면 PDFium은 여러분이 정성껏 편집한 /OFF 배열을 무시합니다. 저장은 성공하고 파일은 깔끔히 다시 열리는데도 레이어는 계속 그려집니다. Print와 Export의 경우 OcExplicitUsageState는 PrintState나 ExportState를 먼저 읽고 그 항목이 없으면 ViewState로 폴백하므로, 홀로 있는 ViewState /ON은 인쇄에서도 레이어를 못박습니다. OCMD(§8.11.2.2)를 참조하는 marked content는 이 그룹별 결과에 대해, /P 정책 또는 있다면 /VE 가시성 식으로 해석됩니다

PDFium이 실제로 보여 줄 레이어는 어떻게 나열할까요?

TPdf.InspectOptionalContent는 TPdfOptionalContentInventory를 반환하는데, 그 Groups 배열은 각 OCG의 객체 번호, 이름, intent, 세 가지 Usage 상태, 언어, 줌 범위, Locked 플래그, 라디오 그룹 인덱스, 계산된 EffectiveVisible을 실어 나릅니다. 메서드는 먼저 PDFium이 현재 메모리상 문서를 저장하게 하고, 객체 스트림을 확장한 뒤 결과를 스캔하므로, 세션에서 앞서 이뤄진 편집이 반영됩니다. 구성 인덱스 0은 언제나 기본 /D 딕셔너리이고 /Configs의 항목은 인덱스 1부터 이어지며, 기본 인자 -1은 인덱스 0을 고릅니다. /OCProperties가 없는 문서는 예외 대신 ErrorMessage에 이유를 담아 False를 반환하게 만듭니다

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에서 멈추고 그보다 깊은 것은 숨긴 것으로 쳐서, 적대적이거나 자기 참조하는 식이 검사를 스택 오버플로로 만들지 못합니다

SaveAsOptionalContentConfigured로 새 레이어 상태 쓰기

TPdf.SaveAsOptionalContentConfigured는 TPdfOptionalContentStateChange 레코드 배열(그룹 객체 번호 + Visible)을 받아 선택된 구성이 정확히 그 상태를 내놓는 문서를 씁니다. 선택된 구성은 /BaseState /ON과 모든 그룹을 커버하는 완전한 /ON, /OFF 배열을 받고, 이미 Usage 딕셔너리를 갖고 있던 각 OCG는 새 상태에 맞는 명시적 ViewState(또는 Options.Usage를 따르는 PrintState / ExportState)를 받습니다. 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 자체 저장 출력을 바이트 단위 접두사로 유지하고, 다시 쓴 구성 소유자와 Usage 딕셔너리를 지닌 OCG 객체만 뒤에 붙인 다음 새 xref 섹션과 trailer로 마무리합니다. 바이트 하나가 목적지에 닿기 전에 결과는 엄격 로드 정책 아래 별개의 TPdf에서 다시 열리고, 상호 참조 테이블이 검증되지 않으면 메서드가 실패합니다. 파일 오버로드는 한 걸음 더 갑니다. 대상 옆 임시 파일에 쓰고 검증이 성공한 뒤에만 대상을 교체하므로, 거부된 업데이트가 절반짜리 도면을 남기는 일이 없습니다. PDFium Component의 PDF 이름 트리·번호 트리 편집기가 쓰는 것과 같은 검증된 증분 리비전 방식입니다

PDFium Component의 SaveAsOptionalContentConfigured가 레이어가 토글된 Delphi PDF를 쓰는 방식: 상태 변경과 옵션이 들어가고, 선택된 구성이 완전한 ON과 OFF 배열, Usage 상태로 다시 쓰이며, 검증된 증분 리비전이 덧붙고, 무엇이든 쓰기 전에 엄격한 재열기가 검증되어야 합니다
구성 저장은 PDFium 자체 재저장을 바이트 접두사로 유지하고, 다시 쓴 구성 소유자와 새 xref 섹션을 덧붙이며, 목적지를 건드리기 전에 결과를 별개 TPdf에서 다시 엽니다

구성 저장은 무엇을 거부할까요?

구성 저장은 문서 자체가 금지했거나 안전하게 표현할 수 없는 변경은 무엇이든 거부하며, 모든 거부는 목적지를 건드리기 전에 일어납니다. /OCGs에 없는 객체 번호는 즉시 실패합니다. 구성의 /Locked 배열에 등장하는 그룹을 바꾸는 것은 실패하지만, 현재 값을 그대로 다시 진술하는 것은 허용됩니다. EnforceRadioGroups가 켜져 있으면 보이는 멤버가 둘 이상 남는 /RBGroups 집합은 다른 멤버를 조용히 꺼버리는 대신 거부됩니다. 암호화된 문서는 평문 증분 객체가 활성 보안 핸들러를 실을 수 없으므로 거부됩니다. 서명된 문서는 AllowSignedDocument = True를 넘기지 않으면 EPdfError를 냅니다. 페이지가 보여 주는 것을 바꾸면 서명 커버리지나 인증 정책이 깨질 수 있기 때문입니다

PDFium Component의 SaveAsOptionalContentConfigured가 구성된 Delphi PDF를 쓰기 전에 적용하는 거부 게이트: OCG 밖의 객체 번호는 실패하고, 잠긴 그룹은 실패하며, 보이는 멤버가 둘 이상인 RBGroups 집합은 거부되고, 암호화된 문서는 평문 증분 객체를 실을 수 없으며, 서명된 파일은 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;    // /D가 아니라 /Configs의 첫 항목
  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;

배치 작업에 이것을 연결하기 전에 트레이드오프를 알아 두세요. 덧붙는 리비전은 원본 파일 바이트가 아니라 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를 금지하므로, 기본 출력은 PDFium Component의 PDF/A preflight 검증이 어차피 보고했을 문제 하나를 미리 없앱니다

Delphi PDF 뷰어에서 레이어 제어가 앉는 자리

뷰어에서 레이어 제어는 인벤토리가 몰고 가는 체크리스트에 저장 결과 재로드를 얹은 것입니다. 체크리스트는 Groups로 채우고, Locked인 항목은 비활성화하고, 같은 RadioGroupIndex를 공유하는 멤버는 상호 배타로 취급하며, 적용할 때는 TMemoryStream에 쓰고 그 스트림을 TPdf에 다시 로드해서 뷰가 새 상태를 그리게 합니다. TPdf와 TPdfView 사이의 배선은 Delphi에서 PDFium VCL로 기능이 풍부한 PDF 뷰어 만들기에서 다룹니다. 라이선싱, 평가판 다운로드, 나머지 기능 세트는 Delphi용 PDFium Component 제품 페이지에 있습니다