오류 메시지는 Please load the document before using BeginDoc(BeginDoc을 사용하기 전에 문서를 로드하십시오)라고 표시되며, 대부분 두 번째 시도 시 발생합니다. 첫 번째 문서는 문제없이 작성됩니다. 그런 다음 동일한 THotPDF 인스턴스에 두 번째 문서를 시작하라고 요청하면 BeginDoc에서 예외가 발생하고, 메시지는 코드의 목적과는 반대인 문서를 로드하는 것을 가리킵니다. 증상과 메시지 간의 불일치가 이 오류를 해결하기 어렵게 만듭니다. 진짜 원인은 컴포넌트 수명 주기에 있으며, 이 원리를 이해하면 오류는 더 이상 미스터리가 되지 않습니다

THotPDF 인스턴스는 문서 공장이 아닌 단일 문서입니다
데이터베이스 연결을 열어 두고 쿼리를 반복해서 실행하는 것처럼, THotPDF를 한 번 생성한 후 문서를 계속 주입할 수 있는 서비스 객체로 생각하기 쉽습니다. 하지만 그렇지 않습니다. 인스턴스는 빌드 중인 단일 문서를 모델링하며, 내부 상태 머신은 빈 상태에서 문서를 열고 파일을 저장하는 경로를 한 번만 거친다는 가정을 가지고 있습니다. BeginDoc은 그 경로를 열고 인스턴스에 문서가 진행 중임을 표시합니다. EndDoc은 모든 것을 FileName에 직렬화하고 종료합니다. 완료된 동일한 인스턴스에서 BeginDoc을 다시 호출하면 깔끔하게 벗어나지 못한 상태로 다시 진입하라는 요청이 됩니다. 내부적으로 "시작할 준비가 됨"과 "로드된 문서가 있음" 조건이 함께 확인되기 때문에 로드를 언급하는 예외 메시지가 발생합니다
메시지는 오해의 소지가 있지만, 보호 장치는 제 역할을 하고 있습니다. 여전히 문서 중간이라고 생각하는 컴포넌트 위에 새 문서를 시작하는 것을 거부하는 것입니다. 해결책은 보호 장치를 우회하는 것이 아닙니다. 다 쓴 인스턴스를 재사용하지 않는 것입니다
반드시 지켜야 하는 순서에 따른 수명 주기
HotPDF가 처음부터 작성하는 모든 문서는 동일한 네 가지 단계를 거치며 이 순서는 변경할 수 없습니다. Create는 컴포넌트를 할당합니다. BeginDoc은 문서를 열고 구조적 선택을 확정하므로, 전체 파일(페이지 크기, 압축, 암호화, 출력 파일 이름)에 영향을 미치는 모든 것은 Create와 BeginDoc 사이에 설정해야 합니다. 그런 다음 그립니다. 그런 다음 EndDoc은 바이트를 디스크에 기록합니다. Free는 인스턴스를 해제합니다. BeginDoc 전에 배치된 그리기 호출은 표시될 페이지가 없으며, 그 이후에 할당된 전체 문서 속성은 경고 없이 무시됩니다
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.BeginDoc; // 문서를 엽니다
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // invoice.pdf를 쓰고 닫습니다
finally
Pdf.Free; // 하나의 인스턴스, 하나의 문서
end;
end;
이것을 작업 단위로 생각하십시오. 하나의 Create, 하나의 BeginDoc, 하나의 EndDoc, 하나의 Free, 디스크에 있는 하나의 파일입니다. 두 번째 파일을 원할 때마다 새로운 작업 단위를 시작하는 것이며, 이는 새로운 인스턴스를 의미합니다
"재사용"의 진정한 의미: 파일당 새로운 인스턴스
실패하는 버전은 할당을 절약하려고 합니다. 컴포넌트를 한 번 빌드하고, 배치를 반복하며 루프 내에서 BeginDoc과 EndDoc을 호출합니다. 두 번째 반복에서 예외가 발생합니다. 작동하는 버전은 각 출력을 수명이 짧은 고유한 객체로 취급하며, 컴포넌트를 생성하는 할당 비용은 PDF를 배치하고 직렬화하는 작업에 비해 미미하므로 인스턴스를 유지하여 얻을 수 있는 이점은 없습니다
procedure WriteBatch(const Names: TArray<string>);
var
I: Integer;
Pdf: THotPDF;
begin
for I := 0 to High(Names) do
begin
Pdf := THotPDF.Create(nil); // 각 패스마다 새 인스턴스
try
Pdf.FileName := Names[I] + '.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
end;
루프 내부에 있는 try/finally는 검토 시 옹호할 가치가 있는 부분입니다. 한 문서 중간에 BeginDoc이나 그리기 호출에서 예외가 발생하더라도, 다음 반복이 시작되기 전에 해당 반복의 인스턴스는 여전히 해제되므로 하나의 잘못된 레코드가 절반만 만들어진 컴포넌트를 남겨두어 나머지 실행을 망치지 않게 됩니다. "최적화"를 위해 Create를 루프 밖으로 빼내면 원래 버그로 되돌아가 배치 루프의 형태를 띠게 됩니다
기존 파일 수정은 완전히 다른 진입점입니다
완전히 합법적인 "재사용"에 대한 두 번째 해석이 있습니다. 즉, 빈 문서가 아니라 이미 존재하는 PDF를 열고 변경하려는 경우입니다. 이 경로는 BeginDoc을 전혀 거치지 않으며, 이것이 오류 메시지에서 로드를 명시하는 정확한 이유입니다. 파일을 로드하고, 편집한 다음, 원하는 이름으로 저장합니다
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('contract.pdf');
if PageCount > 0 then
begin
Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
Pdf.SaveLoadedDocument('contract-reviewed.pdf');
end;
finally
Pdf.Free;
end;
end;
LoadFromFile은 페이지 수를 반환하며, 0 이하의 값은 로드에 실패했음을 의미하므로 CurrentPage를 건드리기 전에 확인하는 것이 좋습니다. 페어링이 중요합니다. LoadFromFile로 연 문서는 빈 상태에서 작성하는 문서에 속하는 BeginDoc/EndDoc 쌍이 아니라 SaveLoadedDocument로 저장해야 합니다. 이 둘을 혼합하는 것은 원래 오류를 발생시킨 것과 동일한 상태 머신을 혼란스럽게 하는 가장 일반적인 방법입니다. 두 흐름을 정신적으로 분리하십시오. BeginDoc ... EndDoc은 생성하고, LoadFromFile ... SaveLoadedDocument는 편집합니다
파일 잠금 문제는 실재하며 뷰어 창을 종료하는 것이 답이 아닙니다
재사용 오류는 종종 두 번째 불만 사항과 함께 발생하며, 파일을 재생성하는 동일한 워크플로에서 나타나기 때문에 둘이 뒤엉키게 됩니다. 사용자가 방금 생성한 PDF를 열고 Acrobat이나 Foxit에서 열어둔 상태로 둔 다음 재빌드를 트리거합니다. EndDoc이 같은 경로에 쓰기를 시도하지만, 뷰어가 쓰기를 차단하는 읽기 공유를 유지하고 있으므로 운영 체제가 이를 거부하고 액세스 거부 오류가 발생합니다. 이것은 컴포넌트 상태 문제가 아니라 윈도우 파일 잠금 문제이며, 해결 방법이 아닌 실제 답변이 필요합니다
최상위 창을 열거하고 제목이 PDF 뷰어처럼 보이는 모든 것에 WM_CLOSE를 게시하여 순환하는 해결 방법은 잘못된 본능입니다. 프로그램이 소유하지 않은 창을 닫기 위해 프로세스 경계를 넘고, 제목 텍스트로 뷰어를 추측하며, 사용자의 저장되지 않은 주석을 묻지 않고 버릴 수 있습니다. 이 모든 접근 방식을 냄새나는 것으로 간주하십시오. 신뢰할 수 있는 수정 방법은 다른 프로세스가 점유하고 있을 수 있는 경로에 쓰지 않는 것입니다. 같은 디렉토리의 임시 파일에 직렬화한 다음, EndDoc이 성공하면 원자적 이름 바꾸기를 사용하여 제자리로 교체합니다. 뷰어에 여전히 이전 파일이 열려 있으면 이름 바꾸기가 깔끔하게 성공하거나 요란하게 실패하며, 잠금과 싸우는 대신 명확한 메시지를 표시합니다
uses
System.SysUtils, System.IOUtils;
procedure WritePdfAtomically(const FinalPath: string);
var
Pdf: THotPDF;
TempPath: string;
begin
// 대상과 동일한 디렉토리의 임시 파일: 하나의 NTFS 볼륨 내에서의 이름 바꾸기는
// 원자적으로 이름을 바꾸는 반면, 볼륨 간 이동은
// 복사 후 삭제로 저하되어 그 보장을 잃습니다.
TempPath := TPath.Combine(TPath.GetDirectoryName(FinalPath),
TGUID.NewGuid.ToString + '.pdf.tmp');
try
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := TempPath;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
Pdf.EndDoc; // 여기 디스크에 임시 파일이 완성됩니다
finally
Pdf.Free;
end;
// 제자리로 스왑합니다. TFile.Move는 덮어쓰기를 거부하므로 오래된 대상을
// 먼저 지웁니다. 뷰어가 여전히 이전 파일을 가지고 있다면 삭제가
// 좋은 바이트를 건드리기 전에 소리 내어 실패합니다.
if TFile.Exists(FinalPath) then
TFile.Delete(FinalPath);
TFile.Move(TempPath, FinalPath); // 또는: RenameFile(TempPath, FinalPath)
except
if TFile.Exists(TempPath) then
TFile.Delete(TempPath); // 절반만 쓰인 임시 파일을 절대 방치하지 마십시오
raise;
end;
end;
이 코드에 대한 두 가지 정직한 각주가 있습니다. TFile.Move와 고전적인 RenameFile은 모두 소스와 목적지가 동일한 볼륨에 있을 때만 원자적인 윈도우 이름 바꾸기에 매핑되며, 이것이 임시 파일이 TPath.GetTempPath가 아닌 목적지 디렉토리로 이동하는 정확한 이유입니다. 삭제 후 이동 쌍 자체는 하나의 원자적 단계가 아닙니다. 두 파일이 모두 존재하지 않는 짧은 창이 있습니다. 보고서를 재생성하는 데스크톱 앱의 경우 이 창은 관련이 없습니다. 동일한 볼륨에서 더 강력한 계약이 필요한 독자는 ReplaceFile 또는 MoveFileEx를 MOVEFILE_REPLACE_EXISTING과 함께 직접 호출하여 스왑을 단일 호출로 축소할 수 있습니다
문서를 지속적으로 재생성하는 대용량 서버의 경우 더 깨끗한 원칙은 두 실행이 하나의 경로를 놓고 경쟁하지 않도록 각 출력을 고유한 이름(타임스탬프 또는 작업 ID)으로 쓰고, 별도의 보존 정책이 오래된 파일을 정리하도록 하는 것입니다. 패턴은 요청당 하나의 명명 원칙 행입니다
// 요청당 하나의 출력 경로: 두 개의 동시 작업은 결코 같은 이름을 놓고
// 경쟁할 수 없으므로 이름 바꾸기 댄스도 없고 잃을 잠금도 없습니다.
OutName := Format('statement-%s-%s.pdf',
[CustomerId, TGUID.NewGuid.ToString.Trim(['{', '}'])]);
Pdf.FileName := TPath.Combine(OutputDir, OutName);
요청 ID 또는 작업 ID는 주변 프레임워크가 이미 제공하는 경우 GUID만큼 잘 작동하며, 파일 이름을 로그 줄로 쉽게 추적할 수 있게 해줍니다. 어느 쪽이든 원칙은 동일합니다. 작성하는 파일이 작성하는 순간에 자신만의 파일이 되도록 설계하는 것입니다. 잠금은 창을 강제로 닫아서가 아니라 다른 어떤 것도 바이트를 건드리지 않기 때문에 사라집니다
수정의 형태
두 가지 문제를 근본으로 되돌려보면 둘 다 경계를 존중하는 것에 관한 것입니다. 상태 머신 오류는 인스턴스 경계를 존중하기를 원합니다. 하나의 THotPDF, 하나의 문서, 그 다음에는 놓아주고 다른 것을 만드십시오. 파일 잠금 오류는 파일 경계를 존중하기를 원합니다. 다른 것이 읽지 않는 곳에 쓴 다음 결과를 제자리로 옮기십시오. 라이브러리를 패치하거나 데스크톱을 스크립팅할 필요가 없습니다. 둘 다 각 문서를 독립적인 작업 단위로 취급하여, 새로 생성하고, 깨끗하게 쓰고, 해제하는 데서 비롯되며, 이는 컴포넌트의 나머지 부분을 예측 가능하게 만드는 것과 동일한 패턴입니다
여기에 표시된 BeginDoc, EndDoc, LoadFromFile 및 SaveLoadedDocument 호출은 Delphi 및 C++Builder용 HotPDF Component의 일부입니다