Delphi 및 Lazarus용 PDFium Component에서 TPdf.Active := True를 대입하는 것은 PDF 로드에 실패해도 절대 예외를 일으키지 않습니다. TPdf.SetActive가 모든 예외를 잡아 컴포넌트를 비활성인 채로 둡니다. 진짜 오류를 보려면 대신 TPdf.LoadDocument(Options, Report)를 호출하세요. 이 오버로드는 원래 예외를 다시 일으키고 로드 상태, 네이티브 PDFium 오류 코드, cross-reference 테이블을 재구축해야 했는지를 TPdfLoadReport에 채워 넣습니다
문제는 보통 배치 코드에서 표면화됩니다. 테이블 추출 작업이 공유 TPdf 하나로 실제 PDF 13개 폴더를 돌았는데 7개가 실패로 돌아옵니다. 그 7개 파일 중 하나도 실제로 깨진 게 아닙니다. 로드 주변의 except 블록은 한 번도 발화하지 않고, 로그는 엉뚱한 파일 이름을 지목하며, 처음 보이는 오류는 정말 실패한 로드의 여러 줄 뒤에서 속성 읽기로 일어난 비활성 컴포넌트에 관한 맨몸 EPdfError입니다. 별개의 동작 두 개가 겹쳐 이 그림을 만드는데, 둘 다 설계대로 동작한 것입니다
PDF 로드가 실패했을 때 TPdf.Active := True가 예외를 일으키지 않는 이유는?
TPdf.SetActive는 모든 예외 클래스를 삼키고 컴포넌트를 비활성인 채로 두는 try..except로 LoadDocument를 감쌉니다. 삼킴은 일부러입니다. 같은 세터가 폼 디자이너가 IDE에서 Active를 토글할 때도 돌고, 나쁜 경로가 IDE를 크래시시켜서는 안 되기 때문입니다. 런타임에 TPdf.Active는 네이티브 문서 핸들이 존재하는지를 보고할 뿐이므로, 실패한 로드 뒤에는 False를 읽을 뿐 그 이상은 없습니다. 일어났던 것은 무엇이든 사라집니다. 파서의 EPdfError였든, 스트림 오류였든, 반쯤 바인딩된 pdfium.dll의 EAccessViolation이었든요. Delphi에서 pdfium.dll 로드 실패 진단하기에서 묘사한 상세 DLL 메시지는 그들을 삼키지 않는 호출로만 당신의 핸들러에 닿습니다
Pdf.FileName := FileName;
try
Pdf.Active := True; // SetActive는 모든 로드 예외를 삼킵니다
except
on E: Exception do
Log.Add(FileName + ': ' + E.Message); // 절대 실행되지 않습니다
end;
// 실패는 대신 여기서 일반 EPdfError로 표면화됩니다:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));
// 기존 코드의 최소 수정: 대입 직후 Active를 검사하세요.
// v3.122.1부터 LastLoadReport가 삼켜진 오류의 텍스트를 보관합니다
Pdf.Active := True;
if not Pdf.Active then
Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);
실패는 첫 가드된 호출에서 마침내 드러납니다. TPdf.PageCount는 대부분의 문서 속성처럼 CheckActive로 시작하는데, 이는 컴포넌트 이름은 말해도 파일은 말하지 않고 원인은 더 말하지 않는 EPdfError를 일으킵니다. 대입 직후 Pdf.Active를 검사하면 잘못 귀속된 크래시가 정직한 "실패" 항목으로 바뀝니다. PDFiumPas v3.122.1 전에는 그 지점에서 이유가 사라졌습니다. v3.122.1부터 실패한 대입은 오류 텍스트를 실은 plsFailed 리포트로 LastLoadReport를 교체하므로 원인이 살아남습니다. 예외 개체 자체와 바이트 수준 감사는 여전히 다른 진입점을 요구합니다
TPdf 하나를 재사용하면 두 번째 파일부터 실패하는 이유는?
TPdf.FileName은 컴포넌트가 비활성인 동안에만 대입할 수 있으므로, 공유 인스턴스는 두 번째 파일을 로드해 보기도 전에 거부합니다. TPdf.SetFileName은 CheckInactive로 시작하고, 같은 가드가 Password와 FormFill을 보호합니다. 첫 성공 로드 뒤 인스턴스는 활성으로 남고, 다음 대입은 예외를 일으키며, 배치 루프가 그 예외를 잡고 넘어가면 오류는 옛 문서가 아직 열려 있는 동안 새 파일 이름 아래에 떨어집니다. 삼켜진 로드 실패와 섞이면 로그는 현실과 어긋납니다. 13파일 재현에서 공유 인스턴스는 7개 실패를 보고한 반면, 문서마다 새 TPdf.Create(nil)은 13개 모두 열었습니다. 파일 사이에 Active := False를 set하는 것도 동작하지만, 문서당 인스턴스 하나가 모든 파일을 구조적으로 격리합니다
TPdfLoadReport를 받는 TPdf.LoadDocument는 무엇을 주나요?
TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport)는 진짜 예외를 일으키면서 무슨 일이 있었는지 구조화된 형태로도 알려 줍니다. 파일 오버로드는 FileName을 로드하고, 형제 오버로드는 TBytes나 포인터와 크기를 받으며, LoadCustomDocument(AStream, AOwnsStream, Options, Report)는 스트림을 커버합니다. 각각은 옵션을 검증하고, 인스턴스가 비활성인지 검사하고, 헤더, startxref, xref 섹션과 %%EOF 마커의 바이트 수준 감사를 돌린 다음 네이티브 로드를 수행합니다. 감사는 신뢰할 수 없는 PDF를 위한 파서 리소스 예산에서 논의된 것과 같은 종류의 한계로 묶여 있습니다. TPdfLoadOptions.Default는 AuditByteLimit를 256 MiB로, MaxIssues를 256으로, MaxXrefSections를 1024로, MaxXrefEntries를 4,000,000으로 set합니다. 실패 시 메서드는 Report.Status := plsFailed를 set하고 다시 일으킵니다. Report는 제자리에 쓰이므로 내용이 예외를 살아남고, 사본은 TPdf.LastLoadReport에 저장됩니다
리포트 필드들은 배치 로그가 실제로 필요로 하는 질문들에 답합니다. Status는 plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected, plsFailed 중 하나입니다. NativeErrorCode는 FPDF_GetLastError를 담으므로, FPDF_ERR_PASSWORD(4)는 빠졌거나 틀린 비밀번호를 FPDF_ERR_FORMAT(3)으로 보고된 손상 파일과 분리합니다. UsedRecovery, CrossReferenceTableValid와 RecoveryRoute는 PDFium이 xref 테이블을 재구축해야 했는지를 말하고, Issues는 각 감사 발견을 Code, Severity, Offset, ObjectNumber, MessageText와 함께 나열하며, MaxIssues가 목록을 잘랐을 때 IssuesTruncated가 set됩니다
uses
SysUtils, Classes, TypInfo, FPdfView, PDFium;
procedure ProcessBatch(Files, Log: TStrings);
var
I: Integer;
Pdf: TPdf;
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default(plmCompatible);
for I := 0 to Files.Count - 1 do
begin
Pdf := TPdf.Create(nil); // 문서당 인스턴스 하나
try
Pdf.FileName := Files[I];
try
Pdf.LoadDocument(Options, Report);
except
on E: Exception do
begin
// LoadDocument가 예외를 일으켰어도 Report는 채워져 있습니다
if Report.NativeErrorCode = FPDF_ERR_PASSWORD then
Log.Add(Files[I] + ': password required')
else
Log.Add(Format('%s: %s (%s)', [Files[I],
GetEnumName(TypeInfo(TPdfLoadStatus), Ord(Report.Status)),
E.Message]));
Continue;
end;
end;
if Report.UsedRecovery then
Log.Add(Files[I] + ': opened after PDFium rebuilt the xref table');
ExtractTables(Pdf, Log);
finally
Pdf.Free;
end;
end;
end;
plmStrict로 로드해야 할 때는?
조용히 수리된 파일이 거부된 것보다 나쁜 경우, 즉 아카이브 인테이크, 증거 처리, 서명 파이프라인에서는 plmStrict를 쓰세요. PDFium은 파일을 스캔해 개체를 찾는 방식으로 깨진 cross-reference 테이블(ISO 32000-1 §7.5.4)을 조용히 재구축하는데, 뷰어에게는 훌륭하고 주어진 바이트를 정확히 그대로 처리해야 하는 무엇에게는 문제입니다. 네이티브 로드 뒤 컴포넌트는 FPDF_DocumentHasValidCrossReferenceTable을 묻습니다. plmCompatible 모드에서 재구축은 plsLoadedWithRecovery와 plicNativeCrossReferenceRebuild 경고를 냅니다. plmStrict 모드에서는 컴포넌트가 문서를 언로드하고, plsRejected를 set하고, plicStrictModeRejected를 더하고, "Strict PDF load rejected the document"라는 EPdfError를 일으킵니다. Strict 모드는 감사 오류도 모두 거부하고, TPdfLoadOptions.Default(plmStrict)는 RequireFinalEndOfFileMarker를 켜서, 빠진 %%EOF나 마지막 것 뒤의 데이터(§7.5.5)를 경고에서 오류로 승격시킵니다. xref 감사는 PDFium VCL로 개체와 xref 스트림 검증하기의 개체 수준 검사를 보완합니다
function AcceptForArchive(const FileName: string; out Reason: string): Boolean;
var
Pdf: TPdf;
Report: TPdfLoadReport;
I: Integer;
begin
Result := False;
Reason := '';
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
try
Pdf.LoadDocument(TPdfLoadOptions.Default(plmStrict), Report);
Result := True; // 유효한 xref, 감사 오류 없음
except
on E: EPdfError do
begin
Reason := E.Message;
for I := 0 to High(Report.Issues) do
if Report.Issues[I].Severity = plisError then
Reason := Reason + sLineBreak + Format(' at offset %d: %s',
[Report.Issues[I].Offset, Report.Issues[I].MessageText]);
end;
end;
finally
Pdf.Free;
end;
end;
TPdf.LastLoadReport는 어디서 진실을 말하기를 멈출까요?
TPdf.LastLoadReport는 옵션을 받는 LoadDocument 오버로드 뒤에서만 완전합니다. 바이트 감사를 돌리는 것이 그 오버로드들뿐이기 때문입니다. 성공한 Active := True는 바이트 감사 없는 호환 모드 리포트를 쓰므로 AuditAttempted는 False로 남습니다. PDFiumPas v3.122.1 전에는 실패한 것이 아무것도 쓰지 않았으므로, 공유 인스턴스에서 LastLoadReport는 흔히 안심시키는 plsLoaded와 함께 이전 파일을 계속 묘사했습니다. v3.122.1부터 모든 실패한 로드는 리포트를 교체합니다. 예외 없이 컴포넌트를 비활성으로 남기는 실패한 Active := True와, 오류 텍스트와 함께 plsFailed를 기록하는 실패한 평범 LoadDocument나 LoadCustomDocument 호출이요. 역시 감사 없이요. 실전에서 중요한 빈틈이 둘 더 있습니다. 옵션 검증과 CheckInactive는 리포트가 초기화되기 전에 돌므로, 음의 AuditByteLimit나 이미 활성인 인스턴스는 리포트를 남기지 않고 예외를 일으킵니다. 그리고 NativeErrorCode는 PDFium이 실제로 파스를 시도했을 때만 의미가 있습니다. 빠진 파일에는 래퍼가 PDFium이 돌기 전에 예외를 일으키므로, ErrorMessage와 예외 텍스트를 대신 기록하세요
실용적인 규칙은 짧습니다. 비활성 컴포넌트가 받아들일 만한 결과인 디자이너 결합 뷰어에는 Active := True를 유지하세요. 그 외의 모든 곳, 무엇보다 배치와 서버 코드에서는 문서당 TPdf 하나를 만들고, LoadDocument(Options, Report)를 호출하고, 일으키는 예외를 잡아 Report.Status, NativeErrorCode와 오류 수준 Issues를 파일 이름과 함께 기록하세요. 비용은 호출 지점당 몇 줄이고, 모든 실패가 진짜 원인과 함께 올바른 파일로 귀속됩니다
로드 리포트 API, strict 모드와 바이트 수준 감사는 Delphi, C++Builder, Lazarus용 PDFium Component에, 렌더링, 텍스트 추출, 폼 채우기, PDF/A 검증과 함께 실려 나옵니다