HotPDF는 페이지 콘텐츠 스트림의 모든 연산자를 순회하며 VisitLoadedPageContentOperators를 통해 각 연산자를 사용자 코드에 전달합니다. 각 연산자마다 동작을 선택할 수 있습니다: 유지, 삭제, 자신의 바이트로 교체, 순회 중단, 또는 편집 전체 거부입니다. 모든 연산자를 유지하면 다시 작성된 스트림은 원본과 바이트 단위로 완전히 동일한데, 이 특성 덕분에 자신이 만들지 않은 문서에도 이 메커니즘을 안전하게 적용할 수 있습니다
콘텐츠 스트림 편집은 PDF 도구들이 보통 잘못되는 지점입니다. 스트림은 문서 트리가 아니라 스택 머신 위의 연산자 시퀀스이며, 디코딩된 바이트에 대해 정규식을 실행하는 순진한 접근 방식은 문자열 리터럴 안에 연산자처럼 보이는 무언가가 있거나 인라인 이미지에 임의의 바이너리가 들어 있는 순간 깨집니다. 이미 스트림을 토큰화해 둔 비지터에는 그런 문제가 없습니다
연산자 수준 접근으로 실제로 할 수 있는 것은?
세 가지 작업 유형이 있습니다. 감사: 페이지가 알려진 색상 집합 밖의 색을 몇 번 설정하는지 세거나, 임베드되지 않은 폰트로 그려진 텍스트를 찾는 것. 제거: 마크드 콘텐츠로 그려진 워터마크를 삭제하거나 특정 이미지 호출을 걷어내는 것. 그리고 정규화: 한 연산자 형태를 다운스트림 소비자가 더 잘 처리하는 동등한 형태로 교체하는 것입니다
이들 모두는 한 가지 요구사항을 공유합니다: 스트림의 나머지 99퍼센트는 건드리지 않고 남겨 둘 수 있어야 한다는 것입니다. HotPDF의 구문 트리는 소스 사본 하나와 압축된 스팬을 저장하며, 편집은 겹치지 않도록 정렬된 오버레이입니다. 결과를 기록할 때는 건드리지 않은 범위를 그대로 복사하고, 실제로 바꾼 곳에서만 교체 바이트를 내보내므로, 아무것도 바꾸지 않는 왕복은 드리프트를 일으킬 수 없습니다
비지터 계약
인터페이스를 구현하거나 이벤트를 할당한 다음, 감사에는 ApplyChanges를 false로, 재작성에는 true로 설정해 방문 메서드를 호출합니다. 이벤트 데이터는 단계, 연산자 이름, 피연산자 개수, 바이트 스팬, 현재 중첩 상태, 그리고 피연산자를 읽기 위한 대여된 뷰를 담고 있습니다:
uses
HPDFDoc, HPDFContentStream;
procedure TAuditor.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
var
Bytes: TBytes;
begin
if EventData.Phase <> covpOperator then
Exit;
// 마크드 콘텐츠 내부에 그려진 텍스트 표시 연산자를 카운트함
if (EventData.OperatorName = 'Tj') or
(EventData.OperatorName = 'TJ') then
begin
Inc(FTextOperators);
if EventData.State.MarkedContentDepth > 0 then
Inc(FTextInsideMarkedContent);
end;
// 필요할 때만 피연산자를 읽음
if EventData.OperatorName = 'Do' then
if EventData.View.GetOperandBytes(0, Bytes) then
NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));
EventData.Action := coaKeep;
end;
procedure TAuditor.Run(const FileName: string);
var
Pdf: THotPDF;
Stats: THPDFContentOperatorVisitStatistics;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) <= 0 then
Exit;
Pdf.OnContentOperator := HandleOperator;
for I := 0 to Pdf.LoadedPageCount - 1 do
Pdf.VisitLoadedPageContentOperators(I, False, Stats); // 감사 전용
Writeln(Format('%d operators visited, max q/Q depth %d',
[Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
finally
Pdf.Free;
end;
end;
뷰는 왜 변경 가능한 것이 아니라 대여된 것인가?
콜백은 수정 가능한 구문 트리를 절대 받지 않습니다. 현재 연산의 바이트나 특정 피연산자를 요청 시 가져올 수 있는 뷰를 받을 뿐이며, 그게 전부입니다. 모든 변경은 대신 action 필드와 replacement 필드를 통해 이루어집니다
이 간접 구조는 형식적인 절차가 아닙니다. 트리를 직접 변경할 수 있게 하면 비지터가 교체 바이트 예산과 겹침 편집 검사를 우회할 수 있게 되는데, 바이트 스팬 오버레이 모델에서 겹치는 편집은 요란하게 깨지는 대신 조용히 잘못된 출력을 만들어냅니다. 변경을 action 필드를 통해 라우팅하면 비지터의 의도와 무관하게 모든 편집이 동일한 방식으로 검증됩니다
필요할 때만 읽는 방식은 일반적인 경우도 저비용으로 유지합니다. 연산자 이름만 세는 감사는 피연산자 바이트를 전혀 실체화하지 않으며, 이는 수십만 개의 연산이 있는 콘텐츠 스트림에서 중요한 차이를 만듭니다
state 필드는 언제나 입력을 나타낸다
각 이벤트는 그래픽 상태 깊이, 텍스트 객체 깊이, 마크드 콘텐츠 깊이, 호환성 섹션 깊이를 그 연산자가 실행되기 직전 원본 입력에 있었던 그대로 담고 있습니다. 비지터가 이전 연산자를 삭제하거나 교체했더라도 이는 변함없이 성립합니다
상태를 편집된 출력이 아니라 원본 입력에 고정한 것은 실용적인 이점을 노린 의도적인 선택입니다: 같은 문서에 대해 같은 감사를 두 번 실행해도 동일한 결과가 나오며, 편집과 함께 실행되는 감사도 자신의 참조 프레임을 스스로 흔들지 않습니다. 출력에서의 깊이를 알고 싶다면 무엇을 삭제했는지 알고 있으니 직접 계산할 수 있습니다. 반대로 움직이는 출력으로부터 안정적인 입력 위치를 재구성하는 것은 불가능합니다
삭제와 교체, 그리고 이를 보호하는 규칙들
교체값은 순수한 콘텐츠 스트림 바이트이며, 그 구문에 대한 책임은 사용자에게 있습니다. 이를 둘러싼 안전장치는 전체 교체 바이트에 대한 예산, 겹치지 않아야 한다는 요구사항, 그리고 결과를 받아들이기 전에 다시 파싱하는 선택적 출력 검증입니다:
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
begin
EventData.Action := coaKeep;
if EventData.Phase <> covpOperator then
Exit;
// 워터마크 폼을 그리는 연산자를 제거함
if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
EventData.Action := coaDelete
// 벗어난 색상을 하우스 그레이로 정규화함
else if EventData.OperatorName = 'rg' then
begin
EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
EventData.Action := coaReplace;
end
// 이해할 수 없는 것을 만나면 편집 전체를 거부함
else if EventData.OperatorName = 'BI' then
EventData.Action := coaReject;
end;
var
Options: THPDFContentOperatorVisitOptions;
begin
Options := THPDFContentOperatorVisitOptions.Default;
Options.ValidateOutput := True; // 승인 전 재파싱
Options.MaxReplacementBytes := 64 * 1024;
Pdf.OnContentOperator := HandleOperator;
if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
if Stats.Modified then
Pdf.SaveLoadedDocument('cleaned.pdf');
end;
동작 집합에는 두 가지 종료 방식이 이유 있게 포함되어 있습니다. coaStop은 순회를 끝내고 그때까지 결정된 내용을 그대로 유지하는데, 찾던 것을 발견했을 때 적절합니다. coaReject는 방문 전체를 포기하고 페이지를 건드리지 않은 채로 두는데, 재작성 규칙이 다루지 않는 구문을 만났을 때 적절합니다. 인라인 이미지가 대표적인 예입니다: 그 바이너리 페이로드는 색상 정규화 비지터가 이리저리 편집해서는 안 되는 대상입니다
통계 읽기, 그리고 순회가 멈추는 지점
status 필드는 완료, 중단, 거부, 잘못된 입력, 한도 초과, 잘못된 출력, 비지터 오류를 구분합니다. 이들은 서로 다른 문제입니다: 잘못된 출력은 교체 바이트가 재파싱되지 않았다는 뜻이고, 잘못된 입력은 소스 스트림이 애초부터 형식에 맞지 않아 어떤 편집도 시도되지 않았다는 뜻입니다
카운터들은 감사를 자체 검증할 수 있게 해 줍니다. VisitedOperatorCount는 KeptOperatorCount, DeletedOperatorCount, ReplacedOperatorCount를 합한 값과 일치해야 하며, 최대 깊이 수치는 그 페이지가 자세히 들여다볼 만큼 깊이 중첩되어 있는지 알려줍니다. SourceBytes와 OutputBytes를 비교하면 편집이 미친 크기 효과를 숫자 하나로 알 수 있습니다
흔한 작업들에는 비지터를 작성할 필요가 전혀 없는 더 상위 수준의 경로가 있습니다. 실제 리댁션에 필요한 리소스 정리까지 포함해 콘텐츠를 영구적으로 제거하는 방법은 로드된 문서에서의 리댁션과 페이지 스티칭에서 다룹니다. 표시되는 텍스트 교체는 기존 PDF에서 검색 및 교체하기에서 다룹니다. 규칙이 텍스트가 아니라 구조적인 경우, 또는 기존 리포트로는 답이 나오지 않는 감사가 필요할 때 연산자 비지터를 사용하세요
콘텐츠 스트림 방문, 추출, 재작성은 Delphi와 C++Builder에서 동일한 로드된 문서 모델을 대상으로 동작합니다. 전체 기능 목록은 HotPDF Delphi PDF 컴포넌트 페이지에서 확인할 수 있습니다