PDFium Component는 편집 저널을 통해 PDF 편집기에 진짜 문서 수준의 실행 취소와 다시 실행, 그리고 크래시 복구 패키지를 제공합니다. BeginEditTransaction과 CommitEditTransaction으로 변경 사항을 감싸면, 커밋된 각 트랜잭션은 UndoEdit과 RedoEdit이 양방향으로 재생할 수 있는 양방향 델타가 됩니다. SaveRecoverySnapshot은 이 저널을 영속화하므로, 세션 도중에 강제 종료된 애플리케이션도 사용자의 작업을 복원할 수 있습니다
이 기능이 메우는 공백은 구체적입니다. 네이티브 실행 취소 기능은 현재 포커스를 가진 폼 필드에만 적용되므로, 텍스트 상자는 자신의 편집만 기억할 뿐 그 밖에는 아무것도 기억하지 못합니다. 페이지 삽입, 삭제, 재정렬, 페이지 객체 변경, 주석, 첨부 파일, 메타데이터에는 저널이 전혀 없습니다. 이것만으로 만든 편집기는 필드 안에서만 실행 취소를 제공할 수 있는데, 이는 사용자들이 실행 취소라고 부르는 것과는 다릅니다
왜 연산 로그가 아니라 스냅샷인가?
명백한 설계는 '4페이지 삭제됨', '주석 7 이동됨' 같은 의미론적 연산을 기록하고 이를 역으로 뒤집는 것입니다. 이는 연산 집합이 커지기 전까지는 동작하는데, 그 시점부터는 새 기능마다 역연산이 필요해지고, 역연산이 정확하지 않은 연산이 하나라도 있으면 긴 실행 취소 체인 동안 문서를 조용히 손상시킵니다
대신 저널은 트랜잭션 경계에서 문서 상태를 캡처하고 그 차이를 저장합니다. 각 델타는 가장 긴 공통 접두사와 접미사를 유지하고 제거된 바이트와 삽입된 바이트를 모두 담으므로, 앞으로도 뒤로도 적용될 수 있습니다. 새로운 편집 기능은 실행 취소 지원이 전혀 필요 없습니다: 그것이 바꾸는 것은 무엇이든 스냅샷 안에 들어 있기 때문입니다
이 접근 방식은 한 가지 전제 조건에 의존합니다. 일반적인 전체 저장은 비결정적인 식별자와 시간 필드를 만들어내므로, 수정되지 않은 문서를 두 번 저장해도 서로 다른 바이트가 나올 수 있습니다. 그래서 스냅샷은 먼저 재현 가능한 출력 경로를 거쳐 정규화됩니다. 이 단계가 없다면 빈 트랜잭션도 수정처럼 보이고, 실행 취소는 커밋 시점에 계산한 다이제스트가 이후 문서와 일치하지 않아 자체 검증에 실패하게 될 것입니다
실제 트랜잭션의 모습
각 트랜잭션은 선택적인 레이블을 받으며, 이것이 편집기가 실행 취소 메뉴에 표시하는 내용입니다:
uses
PDFium;
var
Pdf: TPdf;
Options: TPdfEditJournalOptions;
Info: TPdfEditJournalInfo;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Options := TPdfEditJournalOptions.Default;
Options.MaxEntries := 50; // 실행 취소 깊이
Options.MaxStoredBytes := 256 * 1024 * 1024;
Pdf.ConfigureEditJournal(Options);
Pdf.BeginEditTransaction('Delete cover page');
try
Pdf.DeletePage(1);
if not Pdf.CommitEditTransaction then
raise Exception.Create('commit failed - document was reloaded');
except
Pdf.RollbackEditTransaction;
raise;
end;
Info := Pdf.GetEditJournalInfo;
Writeln(Format('entries=%d position=%d stored=%d bytes',
[Info.EntryCount, Info.Position, Info.StoredBytes]));
if Pdf.EditCanUndo then
Pdf.UndoEdit; // 페이지가 되돌아옴
finally
Pdf.Free;
end;
end;
캡처가 실패했거나, 예산을 초과했거나, 델타를 만들 수 없어서 커밋이 실패하면, 활성 트랜잭션이나 절반만 기록된 저널을 남기는 대신 BeginEditTransaction에서 캡처한 기준선으로 되돌리고 다시 로드합니다. 이것이 가장 중요한 실패 모드인데, 일관되지 않은 상태의 저널은 이후 모든 실행 취소를 오염시키기 때문입니다
모든 단계에서의 검증
각 델타는 이전과 이후 문서의 SHA-256 다이제스트를, 접두사 길이와 두 페이로드와 함께 저장합니다. 실행 취소, 다시 실행, 가져온 저널의 재생, 역방향 재구성은 모두 무언가를 건드리기 전에 현재 다이제스트와 교체하려는 영역을 확인한 다음, 이후에 결과 다이제스트를 검증합니다
저널 안에서의 이동도 똑같이 신중합니다. 위치는 대상 PDF가 독립적인 문서 객체에 의해 검증되고 현재 문서를 성공적으로 대체한 뒤에만 전진합니다. 이것이 실패하면 현재 스냅샷이 폴백으로 복원되므로, 실패한 실행 취소는 사용자를 망가진 상태가 아니라 원래 있던 자리에 남겨 둡니다
직렬화된 형식은 신뢰할 입력이 아니라 데이터로서 검사됩니다: 매직 넘버, 버전, 항목과 위치 값, 다이제스트 체인, 레이블 길이, 패치 길이, 총 저장 예산이 모두 검증됩니다. 가져온 저널은 잘렸거나 편집되었거나 다른 버전에서 생성되었을 수 있는 파일이므로, 이를 적대적인 것으로 취급하는 것이 유일하게 안전한 태도입니다
저널이 가득 차면 어떻게 되는가
세 가지 한도가 성장을 제한합니다: 항목 개수, 누적 델타 바이트, 단일 스냅샷의 크기입니다. 용량에 도달하면 가장 오래된 항목이 제거되고 루트 지문이 전진합니다
이 마지막 부분에는 설계 시 고려할 가치가 있는 결과가 따라옵니다. 내보낸 저널은 그것이 보고하는 루트 지문 또는 그것이 기록한 지문과 일치하는 문서에만 적용될 수 있습니다. 제거가 한 번이라도 일어나면 저널은 더 이상 원본 파일까지 거슬러 올라갈 수 없습니다. 그러므로 자동 저장은 일치하는 루트 스냅샷과 저널을 하나의 원자적 복구 단위로 취급해야 합니다 — 기반 없이 저널만 남기는 것은 재생할 수 없는 것을 남기는 것과 같습니다
크래시 복구 패키지
복구 패키지는 의도적으로 원본 PDF를 임베드하지 않습니다. 원본 표시 이름, 원본 상태와 복구된 상태의 SHA-256 지문, 그리고 편집 저널을 저장합니다. 따라서 복구에는 원본 파일이 필요하며, 호출자가 이를 먼저 로드하면, 패키지는 저널을 앞뒤로 재생해 두 지문을 모두 검증합니다:
// 커밋이 성공할 때마다 복구 파일을 갱신함
Pdf.BeginEditTransaction('Redact account numbers');
if Pdf.CommitEditTransaction then
Pdf.SaveRecoverySnapshot('contract.recovery');
// 다음 세션, 크래시 이후: 원본을 로드한 다음 복원함
Pdf.FileName := 'contract.pdf';
Pdf.LoadDocument;
Pdf.RestoreRecoverySnapshot('contract.recovery');
원본을 임베드하지 않으면 복구 파일이 작게 유지되며, 이 덕분에 어떤 크기의 문서에서도 커밋마다 자동 저장하는 것이 감당할 만해집니다. 헤더, 원본 이름, 저널을 아우르는 외부 다이제스트가 있으므로, 잘렸거나 변조된 패키지는 델타가 파싱되기도 전에 거부되며, 총 읽기량에는 상한이 있습니다
파일 기록은 대상 디렉터리의 임시 파일로 이루어진 다음 원자적으로 교체되며, 복구 경로를 원본 PDF로 설정하는 것은 명시적으로 거부됩니다. 자동 저장은 변경 사항이 있는 성공한 커밋, 실행 취소, 다시 실행 이후에만 실행됩니다. 디스크 쓰기가 실패해도 메모리 안의 문서와 저널 상태는 이미 유효하므로, 예외는 데이터 손실이 아니라 영속화 실패를 보고하며, 호출자는 경로를 고치고 다시 시도할 수 있습니다
서명된 문서는 기본적으로 거부된다
트랜잭션 스냅샷은 원본 서명된 바이트가 아니라 새로 정규화된 리비전이므로, 서명된 문서에 트랜잭션을 커밋하면 서명이 무효화됩니다. 그런 문서에서 트랜잭션을 시작하는 것은 호출자가 AllowSignedDocument를 전달하지 않는 한 거부되며, 저널 가져오기와 복구 복원도 기본적으로 같은 거부 동작을 따릅니다
이는 금지가 아니라 의도적인 과속방지턱입니다. 일부 워크플로는 서명된 문서를 정당하게 편집한 다음 나중에 다시 서명하며, 이 플래그는 그런 경우를 위해 존재합니다. 이것이 막는 것은 사용자가 여전히 서명되어 있다고 믿는 문서에서 편집기가 조용히 유효성을 벗겨내는 우발적인 경우입니다. 이 이야기의 검증 쪽 측면은 PDF 디지털 서명과 PAdES 레벨 검사하기에서 다룹니다
편집기라면 저널이 값어치를 하게 만드는 습관이 세 가지 있습니다. 사용자에게 보이는 각 동작을 레이블이 붙은 하나의 트랜잭션으로 감싸서, 실행 취소 메뉴가 사용자의 콜 스택이 아니라 사용자의 사고 모델처럼 읽히게 하세요. 항목 한도는 문서에 맞춰 정하세요. 200MB짜리 스캔본의 스냅샷 50개는 소박한 설정이 아니기 때문입니다. 그리고 타이머가 아니라 모든 커밋마다 복구 스냅샷을 기록하세요. 타이머는 마지막 틱 이후에 일어난 작업이 정확히 손실된다는 것을 보장할 뿐이기 때문입니다. 이로부터 이득을 보는 문서 수준 편집 연산은 암호화된 PDF의 증분 업데이트에서 다룹니다
편집 저널링, 복구 패키지, 그리고 그 기반의 편집 기본 요소는 Delphi, C++Builder, Lazarus를 위한 동일한 컴포넌트로 제공됩니다. 전체 API는 Delphi용 PDFium Component 페이지에서 설명합니다