기술 문서

Delphi에서 PDF 마크드 콘텐츠 읽고 쓰기

마크드 콘텐츠는 ISO 32000-1 §14.6이 페이지 콘텐츠를 태깅하기 위해 정의한 메커니즘이며, 태깅된 PDF와 PDF/UA는 모두 그 위에 구축됩니다. PDFium Component는 그것을 직접 노출합니다. PageObjectMarks가 페이지 객체에서 모든 BDC 태그와 그 속성 목록을 읽고, AddPageObjectMark가 하나를 쓰며, RemovePageObjectMark가 하나를 삭제하고, PageObjectMarkedContentID가 콘텐츠를 구조 트리에 연결하는 MCID를 보고합니다

구조 트리가 그것이 기술하는 콘텐츠에 다시 연결되기 전까지는 접근성 도구가 추측에 불과합니다. 구조 트리는 이것은 제목이라고 말하고, MCID는 그 제목이 실제로 어느 페이지의 어느 마크인지 말합니다. 애플리케이션이 태깅을 점검하고 보수하고 보고하려면 양쪽 모두 읽을 수 있어야 합니다

바이트 단위로 본 마크는 무엇인가

태그 이름과 선택적 속성 목록을 가진 BDC 연산자이며 EMC로 닫힙니다. 콘텐츠 스트림에서는 /P <</MCID 3>> BDC ... EMC처럼 보입니다. 태그 /P가 역할을 이름 짓고, 딕셔너리가 속성을 담으며, 연산자 사이의 모든 것이 마크드 콘텐츠입니다. 그 범위 안의 페이지 객체가 마크를 담으며, 이것이 PDFium이 돌려주는 것이자 PDFium Component가 레코드로 바꾸는 것입니다

TPdfContentMark는 핸들, 태그 Name, TPdfContentMarkParam의 배열을 담습니다. 각 매개변수는 Key, Kind, 그 종류가 선택하는 하나의 의미 있는 값 필드를 가집니다. pmpInt, pmpFloat, pmpString, pmpBlob가 그것입니다. 종류는 어떤 게터가 우연히 성공했느냐가 아니라 PDFium 자체의 타입 보고에서 오며, 이것이 속성 목록을 읽는 것과 추측하는 것의 차이입니다

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

pmpUnknown가 두 가지 다른 것을 의미하는 이유

pmpUnknown는 PDFium이 FPDF_OBJECT_UNKNOWN을 보고할 때 반환되며, PDFium은 존재하지 않는 키에 대해서도 그것을 반환합니다. 두 사례는 이 층에서 구별할 수 없으며, 그렇지 않은 척하는 것은 그렇게 말하는 것보다 못합니다

코드에 대한 실질적 결과는 이것입니다. pmpUnknown를 어쩌면 어찌어찌 디코딩할 수 있는 타입이 아니라 여기에 사용 가능한 값이 없는 것으로 취급하십시오. 속성이 워크플로에 중요하다면 인식하는 종류로 그것이 있는지 검증하고, 알려지지 않음에서 부재를 추론하지 마십시오. 읽을 수 없는 속성 목록을 가진 마크는 조용히 받아들여야 할 마크가 아니라 보고해야 할 마크입니다

마크 레코드는 소유하는 핸들이 아니라 스냅샷이다

Handle 필드는 라이브러리에 속합니다. 마크가 제거되거나 페이지 객체가 파괴되거나 페이지가 언로드되는 순간 오래된 것이 되므로, 레코드는 짧은 수명을 가진 읽기 전용 스냅샷입니다. 페이지 전환을 가로질러 캐시하면 엔진이 되찾은 메모리를 가리키는 포인터를 쥐게 됩니다

이것은 PDFium에서 페이지 객체 핸들에 일반적으로 적용되는 훈련과 같으며, 같은 곳에서 사람을 잡습니다. 마크 레코드로 채워진 목록 컨트롤, 다른 페이지로 이동하는 사용자, 그리고 탐색과 관련 없어 보이는 충돌이 그것입니다. 필요한 값, 즉 이름, 키, 숫자를 복사해 내고 핸들을 놓으십시오. 변환 후 오래되는 페이지 객체 핸들에 관한 글이 일반 규칙과 그것이 다른 곳에서 어떻게 무는지를 다룹니다

마크 추가, 그리고 놓치기 쉬운 저장 단계

AddPageObjectMark는 페이지 객체 인덱스, 태그 이름, 완전한 매개변수 집합을 받습니다. 매개변수는 한 번에 하나의 키를 패치하는 대신 집합으로 기록되며, 이것이 TPdfContentMarkParamHas* 센티넬이 없는 이유입니다. 그런 것이 지킬 기존 레코드의 한 필드를 갱신하는 사례는 일어나지 않습니다

명시적으로 말할 가치가 있는 부분은, 마크를 추가하면 태그가 저장을 견디도록 페이지 콘텐츠 스트림을 다시 구축한다는 것입니다. 이것이 명시적이어야 했던 것은 SaveAs가 자체적으로 콘텐츠를 다시 생성하지 않기 때문입니다. 객체 모델에만 살았던 변경은 버려지며, 저장된 파일은 시작한 것과 정확히 같게 보일 것입니다. PDFium 페이지에 무언가를 추가하고 출력에서 빠진 것을 발견했다면, 보통 이것이 이유입니다

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

이것이 문서를 만들고 만들지 않는 것

마크만으로는 태깅된 PDF가 되지 않습니다. 규칙을 준수하는 태깅된 문서는 이 MCID를 참조하는 요소를 가진 구조 트리, 문서가 마크되었다고 선언하는 /MarkInfo 항목, 표준이 말하는 의미를 가지는 역할 이름이 필요합니다. 어느 구조 요소도 가리키지 않는 MCID를 가진 /P 마크를 기록하면 태깅되었다고 주장하는 콘텐츠와 그것을 결코 언급하지 않는 구조 트리를 얻습니다

이 수준에서 마크드 콘텐츠가 진정으로 가치를 발휘하는 곳은 점검과 보수입니다. 어느 페이지 객체가 태깅되었는지 감사하고, 아티팩트로 표시되었어야 할 것을 찾고, 구조 트리에 MCID를 매칭하여 고아를 찾는 것입니다. 그 작업의 구조 트리 절반은 PDF/UA 구조 트리 검증 산책문을 보고, 태그가 궁극적으로 봉사하는 독서 경험은 Delphi에서 접근성 PDF 리더 구축에 관한 글을 보십시오

PDFium Component는 Delphi, C++Builder, Lazarus 애플리케이션에 PDFium 엔진 위의 고수준 VCL API를 제공하며, 마크드 콘텐츠, 구조 트리, 접근성 검증이 일반 Pascal 코드에서 도달할 수 있습니다. 전체 API 표면은 PDFium Component 제품 페이지를 보십시오