누군가 이름 위에 검은 상자를 칠하고, 아무것도 평탄화하지 않고, 파일을 출고하면, 검토자는 사각형을 선택해 이름을 이메일에 붙여 넣습니다. PDFiumPas는 연산자 수준 레닥션으로 그것에 답합니다. SaveAsRedacted는 문자 박스가 레닥션 사각형에 닿는 유니코드 스칼라만 지우고, 살아남은 자들을 원래 폰트, 크기, 행렬, 렌더 모드, 색에서 재구축하며, 축 정렬 패스와 이미지는 통째로 버리는 대신 자릅니다
칠한 사각형이 레닥션이 아닌 이유
콘텐츠 스트림 위에 얹힌 드로잉 연산은 아무것도 숨기지 않습니다. 그 아래 텍스트 표시 연산자가 여전히 스트림 안에 있고 여전히 코드 포인트로 매핑되기 때문입니다. ISO 32000-1 §9.4는 텍스트 객체를 BT와 ET 안의 위치 지정 및 표시 연산자 시퀀스로 정의합니다. 그 뒤에 그려진 채워진 사각형은 같은 스트림의 또 다른 연산자일 뿐입니다. 추출은 픽셀이 아니라 연산자를 걷기 때문에 가려진 문자열은 온전하게 돌아옵니다. 진짜 레닥션은 출력을 흐리는 것이 아니라 피연산자를 제거해야 합니다
명백한 안전 구현은 브루탈합니다. 경계 박스가 레닥션 사각형과 교차하는 모든 페이지 객체를 찾아 객체 전체를 지우는 것입니다. 이전 PDFiumPas 릴리스가 그렇게 했고, 올바르지만 비쌉니다. 하나의 Tj가 표 행 전체를 실을 수 있으므로, 계좌번호 하나를 검게 칠하면 날짜와 설명과 금액이 함께 갔습니다. 전체 폭 표 밴드인 직사각형 채우기는 페이지 전체에서 사라졌습니다. 송장 로고는 레닥션이 모서리 하나를 잘라 없어졌습니다. 버전 3.101.0은 결정을 한 계층 아래, 페이지 객체에서 피연산자로 내립니다
연산자 수준 레닥션은 실제로 무엇을 지우는가
PDFiumPas는 텍스트 객체가 아니라 유니코드 스칼라를 지웁니다. SaveAsRedacted 동안 컴포넌트는 적재된 텍스트 페이지에서 문자-페이지-객체 매핑을 만들고, 검사 중인 객체가 소유한 모든 문자에 대해 문자 박스를 읽어 각 레닥션 사각형과 교차시킵니다. 사각형에 닿는 문자는 제거 표시되고, 나머지는 생존자 표시됩니다. 교차가 없으면 객체는 완전히 그대로 남습니다. 모든 문자가 교차하면 객체는 예전과 정확히 같이 통째로 제거됩니다. 뒤섞인 경우만이 분할을 촉발합니다
각 생존자는 이어 원래 폰트 핸들, 원래 폰트 크기, 문자별 텍스트 행렬, 원래 텍스트 렌더 모드, 획 너비, 라인 조인, 라인 캡, 대시 배열을 포함한 부모 객체의 채우기와 획 상태로 만들어진 자기 텍스트 객체로 다시 발행됩니다. 새로 해석하는 대신 폰트 핸들을 재사용하는 것이 글리프를 계량적으로 동일하게 유지하고, 문자별 행렬을 재사용하는 것이 레이아웃을 다시 돌리지 않고 커닝과 단어 간격을 제자리에 유지합니다. 비용은 객체 개수입니다. 유지된 문자 하나가 텍스트 객체 하나가 되는데, TPdfRedactionOptions.MaxSplitObjects가 생성되는 파편의 하드 천장으로 존재하는 이유입니다
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // 파일이 이미 /Redact 주석을 실고 있다
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // 연산자 수준 분할(기본값)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // 생성 파편의 천장
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, 출고하지 않는다
finally
Pdf.Free;
end;
end;
사각형은 잘리고, 회전한 기하는 안 된다
패스는 PDFiumPas가 그 패스가 축 정렬 사각형임을 증명할 때만 분할됩니다. 증명은 일부러 좁습니다. 객체 행렬의 전단 항 둘 다가 0.0001 미만이어야 하고, 패스는 MOVETO로 시작해 LINETO만으로 이어지는 4~6 세그먼트여야 하며, 변환된 점들은 0.01 관용 안에서 객체 경계의 네 모서리 전부에 착지해야 합니다. 그 검사를 통과한 패스는 연속 사각형 뺄셈으로 축소되고, 각 레닥션 사각형은 생존자 집합을 왼쪽, 오른쪽, 아래, 위 스트립으로 새기며, 결과 스트립마다 원래 채우기 모드, 획 플래그, 칠하기 상태로 다시 만들어집니다. 곡선, 삼각형, 클립된 모양, 회전된 모든 것은 검사에서 떨어지고 객체 전체가 제거됩니다
이미지는 ISO 32000-1 §8.9를 따릅니다. 이미지 샘플이 현재 변환 행렬을 통해 매핑되는 단위 정사각형을 점유하는 방식입니다. PDFiumPas는 그 매핑을 뒤집어 살아남은 각 페이지 공간 파편을 정규화된 이미지 좌표로 되돌리고, 단위 구간으로 클램프한 뒤, 안쪽으로 반올림해 픽셀 인덱스로 바꿉니다. 왼쪽과 위 가장자리는 Ceil을 거치고, 오른쪽과 아래는 Floor를 거칩니다. 그 방향이 중요합니다. 바깥쪽으로 반올림하면 레닥션 쪽 소스 픽셀의 부분 열이 파편 가장자리에서 살아남을 수 있습니다. 정수 픽셀 경계는 이어 다시 정규화 좌표로 바뀌어 파편 행렬을 도출하는 데 쓰이므로, 잘린 비트맵은 잘린 그 픽셀 경계 위에 정확히 착지합니다. 자르기 자체는 Gray, BGR, BGRx, BGRA 포맷에 걸친 스트라이드 인식 행 복사입니다. 패스와 마찬가지로, 회전하거나 비스듬한 이미지, 또는 행렬에 퇴화 스케일 항이 있는 이미지는 통째로 제거됩니다
// 성공한 SaveAsRedacted 호출 뒤
Writeln(Format('applied %d redaction(s) on %d page(s)',
[Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
[Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
[Report.SplitTextObjectCount, Report.SplitPathObjectCount,
Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
[Report.ResourcePruneReport.RemovedNameCount,
Report.ResourcePruneReport.RemovedObjectCount]));
if Report.PreservedFragmentCount = 0 then
// 분할할 수 있는 것이 없었다: 교차하는 모든 객체가 통째로 버려졌다
LogWholeObjectFallback(SourcePdf);
PDFiumPas는 매핑 없는 문자에서 왜 fail closed 하는가
재현 가능한 유니코드 스칼라가 없는 글리프는 정직하게 재구축될 수 없기 때문입니다. 생존자를 재구축한다는 것은 문자열로 텍스트 설정 API를 부른다는 것이고, 그것은 유지된 모든 문자에 대해 안정적인 코드 포인트를 요구합니다. 깨졌거나 없는 ToUnicode 데이터를 가진 심볼릭 서브셋 폰트는 빈 매핑을 낼 수 있고, 추측으로 재인코딩하면 아래에 다른 문자를 실은 채 화면에서는 올바르게 보이는 출력이 나옵니다. PDFiumPas는 거절합니다. 유지 문자 검사가 예외를 던지고, 그 예외는 SaveAsRedacted 안에서 잡히며, TPdfRedactionReport.Succeeded는 ErrorMessage의 메시지와 함께 False로 돌아오고, 함수는 False를 돌려줍니다. 같은 규칙이 분할 예산에도 적용되는데, 파편 집합을 조용히 잘라내는 대신 예외를 던집니다. 문서에 믿지 못하는 폰트가 있고 결정론적인 옛 행동을 원한다면 Options.PreservePartialObjects := False로 두어 교차하는 모든 객체가 통째로 사라지게 하십시오
공유 스코프에 걸친 리소스 정리
객체를 분할하면 고아가 남고, 그들을 정리하는 것은 페이지 수준 /Resources 사전을 디핑하는 것만큼 단순하지 않습니다. ISO 32000-1 §7.8.3은 같은 리소스 사전이 여러 페이지, Form XObject, 패턴, 주석 외관 스트림에 의해 동시에 참조될 수 있게 합니다. 한 페이지가 쓰기를 그만뒀다고 폰트 이름을 지우면 아직 쓰는 다른 페이지가 깨집니다. 그래서 PruneUnusedPdfResources는 스코프별로 일합니다. /Contents가 직접 배열이든 배열의 간접 참조든 단일 스트림이든 해석하고, 실제로 리소스를 이름 짓는 연산자에서 리소스 사용을 수집합니다 — 폰트는 Tf, XObject는 Do, 그래픽 상태는 gs, 색 공간과 패턴은 CS, cs, SCN, scn, 셰이딩은 sh, 마크드 콘텐츠 속성은 BDC와 DP, 그리고 인라인 이미지의 /CS 항목. 하나의 사전이 여러 스코프에 공유되면, 무엇이든 제거되기 전에 사용된 이름 집합이 범주별로 합집합됩니다
사전을 가리키는 모든 스코프에 걸쳐 참조되지 않음이 확인된 이름만 버려집니다. 자신 있게 파싱할 수 없는 스코프는 손대지 않은 채 남는데, 그것이 보수적인 방향입니다. 정리 안 된 파일은 단지 클 뿐이고, 잘못 정리된 파일은 망가집니다. 살아남은 사전들은 정확한 세대 번호를 실은 희소 증분 업데이트로 다시 쓰이고, 도달 가능성 재작성이 이어서 이름이 사라진 뒤 도달 불가가 된 객체들을 청소합니다. TPdfResourcePruneReport는 ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, 바이트 개수, Succeeded 플래그를 보고합니다. SaveAsRedacted는 이 단계를 정화된 출력에서 자동으로 돌리므로 레닥션 경로에 이미 포함되어 있지만, 함수는 자기만 원하는 파이프라인을 위해 스트림 수준에서 내보내집니다
uses
FPdfCompress;
procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
Source, Dest: TFileStream;
Report: TPdfResourcePruneReport;
begin
Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create(TargetPdf, fmCreate);
try
// AllowSignedDocument는 False로 남는다: 증분 재작성은
// 서명이 덮는 바이트 범위를 무효화한다
PruneUnusedPdfResources(Source, Dest, Report);
if not Report.Succeeded then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
[Report.RemovedNameCount, Report.UpdatedScopeCount,
Report.SourceByteCount, Report.OutputByteCount]));
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
문서 파이프라인에 연결하기
레닥션 경로는 여러분이 적재한 문서를 절대 변이하지 않습니다. SaveAsRedacted는 격리된 스냅샷을 잡고, 그곳에 /Redact 주석을 적용하고, 첨부를 떼어 내고, 열기 액션, 카탈로그 액션, 이름 트리, 연관 파일, AcroForm, 메타데이터를 제거하는 정화 패스를 돌리고, 리소스를 정리하고, 그 뒤에야 출력 스트림을 씁니다. 그 출력을 독립 문서로 다시 열고 텍스트를 다시 추출하는 것이 여러분 테스트 스위트에 유지할 가치가 있는 검증 단계입니다. 원래 질문 — 독자가 아직 그 문자열을 얻을 수 있는가 — 에 답하는 유일한 검사기 때문입니다. 계획할 하나의 결과. 분할은 페이지 객체를 교체하므로, 쥐고 있던 어떤 FPDF_PAGEOBJECT 핸들이든 그 뒤에는 죽어 있습니다. 변환 뒤 오래된 페이지 객체 핸들에서 기술한 같은 수명 함정입니다
이웃한 두 조각이 워크플로를 완성합니다. 레닥션 사각형이 어디로 갈지 결정하는 것은 흔히 추출된 기하에서 시작하는데, 구조화 텍스트 블록과 읽기 순서의 블록과 읽기 순서 모델이 날 문자열 런보다 후보 박스의 더 좋은 근원입니다. 결과를 검토자에게 서빙하는 것은 안전한 PDF 미리보기 만들기의 하드닝 규칙에 속하는데, 거기서는 폼 작성과 JavaScript가 기본으로 꺼져 있습니다. 함께 그들이 대부분의 컴플라이언스 워크플로가 필요로 하는 루프를 덮습니다. 찾고, 연산자 수준에서 가리고, 다시 열어 검증하고, 안전하게 미리 봅니다. 컴포넌트의 전체 API 표면, 평가판 다운로드, 라이선스 조건은 PDFium Delphi Component 제품 페이지에 있습니다