기술 문서

북마크를 깨뜨리지 않고 델파이에서 PDF 페이지 교체

승인 완료된 계약서의 3페이지를 교체하는 것이 목차를 움직여서는 안 됩니다. 이전 페이지를 삭제하고 새 페이지를 삽입하면, 예전에 그곳을 가리키던 모든 북마크가 이제 다른 어딘가에 착지합니다. PDFlibPas 델파이 PDF 라이브러리는 대상 페이지 객체 자체를 유지하고 시각적 콘텐츠를 담은 항목만 전송함으로써 이를 피합니다

PDF 페이지를 교체한 뒤 북마크가 깨지는 이유는 무엇인가

PDF 목적지는 페이지 번호가 아니라 간접 객체 참조로 페이지 이름을 붙이기 때문에 북마크가 깨집니다. ISO 32000-1 §12.3.2.2는 명시적 목적지를 첫 번째 요소가 페이지 객체에 대한 간접 참조인 배열로 정의합니다. 그 객체를 삭제하고 대체본을 덧붙이면 그 참조는 매달린 채로 남습니다: 대부분의 뷰어는 리더를 1페이지에 떨어뜨리는 것으로 반응하는데, 이는 정확히 삭제-후-삽입 방식의 교체 뒤에 사람들이 보고하는 증상입니다. 페이지 트리는 완벽해 보이고, 페이지 개수는 맞으며, 렌더링도 맞지만, 탐색 계층 전체가 조용히 잘못되어 있습니다

이름 붙은 목적지도 여러분을 구해주지 않습니다. §12.3.2.3은 문서 카탈로그의 /Dests 이름 트리를 통해 이름을 라우팅하지만, 그 이름이 해석되는 리프는 여전히 같은 페이지 참조를 담은 명시적 목적지 배열입니다. 이름 붙이기는 페이지 참조 주위가 아니라 그 위에 간접 계층을 하나 더할 뿐입니다. §12.5에서 설명하는 대화형 계층의 나머지도 같은 이유로 영향받습니다: 링크 주석은 그 /D가 바로 그 배열인 /Dest/A GoTo 액션을 담고, 모든 주석은 자신의 페이지에 대한 간접 참조인 /P 항목을 가질 수 있으며, 폼 필드 위젯도 정확히 같은 입장의 주석입니다. 순진한 페이지 교체 하나가 네 개의 서브시스템을 한꺼번에 분리시킵니다. 실제 파일에서 그것들이 열거되는 것을 보고 싶다면, 아웃라인과 주석 인트로스펙션이 순회하는 것과 같은 객체 그래프입니다

어떤 페이지 항목이 정체성을 담고 어떤 항목이 외관을 담는가

페이지 딕셔너리는 두 종류의 항목을 섞고 있으며, 제자리 교체는 정확히 여러분이 그것들을 분리할 때 성공합니다. 외관 쪽은 유한하고 열거 가능합니다: /Contents, /Resources, 다섯 개의 페이지 박스 /MediaBox, /CropBox, /BleedBox, /TrimBox, /ArtBox, 그리고 /Rotate, /Group, /UserUnit, /BoxColorInfo입니다. 그 열한 개 항목이 래스터라이저가 그 페이지에 대해 만들어내는 모든 것을 결정하며, 파일 안의 다른 그 무엇도 이름으로 그것들을 가리키지 않습니다

정체성 쪽은 문서의 나머지가 스스로를 묶어놓은 대상입니다: 페이지 객체 번호와 세대, 페이지 트리로 돌아가는 /Parent 역링크, 그리고 /Annots입니다. PDFlibPas는 그것들 하나하나를 손대지 않은 채 유지합니다. ReplacePageRanges는 대상 페이지 딕셔너리에서 열한 개의 시각적 항목을 제거하고 가져온 소스 페이지로부터 다시 추가하므로, 대상 페이지 객체는 교체되는 것이 아니라 제자리에서 변형됩니다. §7.7.3이 요구하는 페이지 트리 구조도 형태상 바이트 단위로 동일하게 유지됩니다: /Kids 순서, /Count, 그리고 살아남은 각 /Parent는 전후가 같습니다. 어떤 노드도 결코 연결 해제된 적이 없기 때문입니다

PDFlibPas는 객체 번호를 재부여하지 않고 어떻게 페이지를 교체하는가

이 호출은 소스 문서, 1 기반 대상 시작 페이지, 소스 범위 표현식, 옵션 플래그를 받습니다. 두 문서 모두 같은 인스턴스에서 열려 있어야 하며, 대상 문서는 선택된 문서입니다. 대상 페이지 개수는 결코 변하지 않으므로, 여러분이 요청하는 범위는 TargetStartPage부터 시작하는 문서 안에 들어맞아야 하며, 이는 무엇이든 생성되기 전에 확인됩니다

var
  Lib: TPDFlib;
  TargetDoc, SourceDoc: Integer;
begin
  Lib := TPDFlib.Create;
  try
    // The document whose bookmarks and links must survive
    if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
      Exit;
    TargetDoc := Lib.SelectedDocument;

    // The revised clause page, rendered by whatever produced it
    if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
      Exit;
    SourceDoc := Lib.SelectedDocument;

    Lib.SelectDocument(TargetDoc);
    // Source page 1 overwrites the visuals of target page 3.
    // Page count, page 3 object number, bookmarks and annotations are kept.
    if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
      Lib.SaveToFile('contract-final.pdf');
  finally
    Lib.Free;
  end;
end;

내부적으로 소스 페이지는 단순히 문서 경계를 넘어 읽힐 수 없습니다. 그 안의 모든 간접 참조가 소스 객체 번호 매김에 속하기 때문입니다. 그래서 소스 범위는 먼저 마지막 실제 페이지 뒤에 덧붙여진 임시 페이지로서 평범한 방식으로 가져와지며, 이는 전체 객체 그래프 재매핑을 실행합니다: 콘텐츠 스트림, 폰트, XObject, 셰이딩, 색 공간이 모두 대상 문서로 재번호 매겨집니다. 오직 그런 다음에야 열한 개의 시각적 항목이 각 임시 페이지에서 그 대상 페이지로 복사되며, 오직 그런 다음에야 임시 페이지가 페이지 트리에서 연결 해제됩니다. 재매핑 작업은 값싸고 안전한 곳에서 일어나며, 파괴적인 편집은 이미 존재하는 페이지에 대한 딕셔너리 수준의 교환으로 축소됩니다

방금 전송한 것을 파괴할 삭제 경로

그 임시 페이지들을 제거하는 것은 사소해 보이지만 그렇지 않은 단계입니다. 라이브러리의 일반적인 페이지 삭제 경로는 노드 연결 해제보다 더 많은 일을 합니다: 삭제되는 각 페이지의 레이어를 결합하고, 첫 콘텐츠 스트림을 비우고, 다른 어떤 페이지도 공유하지 않는 리소스를 회수합니다. 이는 진짜 삭제에 대해서는 올바른 동작이며, 여기서는 파국적입니다. 임시 페이지가 제거될 시점에는 대상 페이지들이 이미 정확히 그 콘텐츠 스트림과 리소스 객체를 참조하고 있기 때문입니다. 그것들을 비우면 여러분이 방금 교체한 페이지가 백지가 될 것이고, 리소스 스윕은 이제 살아 있는 소유자를 가진 폰트와 이미지를 수거해 갈 것입니다

수정은 내부 삭제 경로에 참조된-객체-보존 모드를 두는 것입니다. 이것이 세팅되면, 삭제는 비공유 리소스 스윕과 콘텐츠 스트림 지우기를 모두 건너뛰고, 페이지를 페이지 트리에서 분리하고 트리 장부를 정리하는 것 외에는 아무것도 하지 않습니다. 전송된 객체들은 새 소유자와 함께 살아남으며, 연산 이후의 객체 소유권은 여러분이 화이트보드에 그릴 만한 것입니다: 하나의 콘텐츠 스트림, 하나의 소유 페이지, 결코 이동하지 않은 하나의 객체 번호입니다. 페이지 생성, 삭제, 재정렬에 관한 관련 생명주기 규칙은 문서 및 페이지 생명주기 연산에 관한 노트에서 별도로 다룹니다

순서, 중복, 전부-아니면-전무 실패

옵션 플래그는 소스 범위가 해석되는 방식을 선택합니다. 0은 파싱된 페이지 번호를 정렬하고 중복을 제거하는데, 이는 호출자가 '4-6,2' 같은 것을 전달할 때 단순히 그 네 페이지를 의미하는 온전한 기본값입니다. 1은 여러분이 쓴 순서를 보존하고 페이지 반복을 허용하므로, '2,1,2'는 진짜로 두 소스 페이지에서 가져온 세 번의 교체를 의미합니다. 검증은 먼저 실행되고 완전하게 실행됩니다: 범위 구문, 소스 페이지 개수에 대한 모든 페이지 번호, 옵션 값 자체, 대상 용량이 단 하나의 객체가 생성되기 전에 모두 확인됩니다. 거부된 호출은 LastErrorCode를 412로 세팅하고, 이전에 선택되었던 페이지를 복원하며, 문서를 정확히 그대로 남겨둡니다

var
  Replaced: Integer;
begin
  Lib.SelectDocument(TargetDoc);
  // Options = 1: source order is preserved and repeats are allowed, so
  // target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
  Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
  if Replaced = 0 then
    raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
      [Lib.LastErrorCode]);
  // On success the selection is the first replaced page
  Assert(Lib.SelectedPage = 5);
end;

원자성은 검증을 넘어 전송 자체까지 이어집니다. 첫 소스 페이지가 가져와지기 전에, 범위 안의 모든 대상 페이지의 열한 개 시각적 항목이 인코딩된 값으로 스냅샷됩니다. 가져오기가 실패하거나 가져온 페이지 개수가 요청된 것과 일치하지 않으면, 그 스냅샷은 대상 페이지에 다시 디코딩되어 얹히고 임시 페이지는 제거되므로, 진행 중 실패도 여전히 원본 시각 자료를 원래 객체 위에 그대로 남겨둡니다. 이는 들리는 것보다 더 중요합니다: 계약서에서 절반만 교체된 페이지 범위는 실패한 호출보다 더 나쁩니다. 파일 안 그 무엇도 그것을 절반만 완료된 것으로 표시하지 않기 때문입니다

// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);

제자리 교체가 여전히 해주지 않는 것은 무엇인가

소스 주석, 소스 폼 필드, 소스 아웃라인은 의도적으로 가져와지지 않습니다. /AcroForm 필드 항목 없이 위젯을 건너오게 하거나, 구조 트리 소유권 없이 마크업 콘텐츠를 지닌 주석을 건너오게 하면 어떤 뷰어도 추론할 수 없는 절반만 가져와진 대화형 객체가 만들어지므로, 이 연산은 외관만 전송합니다. 실질적인 결과는, 교체 페이지가 새로운 폼 필드나 새로운 링크를 담아야 한다면, 여러분이 그것들을 나중에 대상 페이지에 추가한다는 것입니다. 대상 페이지 객체는 여전히 그 자리에 앉아 그것들을 기다리고 있습니다

여러분 자신의 파일에서 확인할 가치가 있는 두 가지 경계가 더 있습니다. 첫째, /Annots는 보존되지만 페이지 기하는 그렇지 않으므로, 220mm 페이지를 320mm 페이지로 교체하면 주석 사각형이 다르게 크기가 매겨진 /MediaBox 안에서 옛 좌표에 그대로 남습니다. 기하가 바뀐다면, 유지한 주석을 재배치하십시오. 둘째, 열한 개의 시각적 키 밖의 항목은 설계상 대상 페이지와 함께 남습니다. 이는 /Trans/AA에는 옳지만 /Thumb에는 오래된 것이므로, 교체 이후 썸네일을 재생성하십시오. 태그된 문서는 추가로 한 가지를 생각해야 합니다: 구조 요소는 여전히 /Pg를 통해 올바른 페이지 객체를 가리키지만, 그 마크업 콘텐츠 식별자는 더 이상 존재하지 않는 콘텐츠를 설명하므로, PDF/UA 워크플로 안에서의 페이지 교체는 콘텐츠 편집이기도 하지만 구조 트리 편집이기도 합니다. 여러분의 작업이 실제로는 교체가 아니라 합성, 즉 유지하는 페이지에 아트워크를 겹치는 것이라면, 페이지 스티칭과 템플릿 접근법이 더 저렴한 도구입니다

범위 표현식 구문, 옵션 값, 주변 페이지 조작 API를 포함해 여기서 설명한 모든 것은 델파이와 C++Builder용 표준 PDFlibPas 델파이 PDF 라이브러리에 포함되어 제공됩니다. 그 레퍼런스 문서에는 페이지 교체 호출과 그 오류 코드에 대한 전체 항목이 있습니다