THotPDF 인스턴스 하나는 문서를 몇 개든 차례로 쓸 수 있습니다: EndDoc 뒤의 BeginDoc은 새로운 독립 문서를 시작합니다. 인스턴스 하나의 다중 문서 재사용은 HotPDF v2.764.0부터 가능합니다. Please load the document before using BeginDoc 메시지는 이웃한 가드의 것입니다: BeginDoc으로 시작한 문서가 아직 EndDoc으로 닫히지 않았을 때 LoadFromFile이 이를 일으킵니다. 두 동작 모두 컴포넌트 수명 주기에서 따라 나오며, 한 번 이해하면 둘 다 신비롭지 않습니다
THotPDF 인스턴스는 문서 하나일까요, 문서 공장일까요?
THotPDF 인스턴스는 문서 공장입니다: 한 번 만들어 두고 — 폼에 얹은 컴포넌트처럼 — 데이터베이스 연결을 열어 두고 쿼리를 이어서 날리듯 문서를 먹일 수 있습니다. BeginDoc은 문서를 열고 인스턴스에 진행 중인 문서가 있다고 표시합니다. EndDoc은 모든 것을 FileName으로 직렬화한 뒤 그 파일을 위해 등록된 모든 것 — 이미지, 폰트, 폼 필드, 아웃라인, 문서 JavaScript, 이름 붙은 대상(named destinations), 페이지 라벨, ICC 프로파일, 출력 인텐트 — 을 내려놓습니다. 기록된 문서의 페이지와 객체는 다음 BeginDoc이 해제하기 전까지 읽을 수 있으므로, ValidatePDFUA2Structure, CurrentPageNumber, PagesCount는 EndDoc 뒤에도 동작합니다. ReproducibleOutput이 켜져 있으면 재사용한 인스턴스의 두 번째 문서는 새 인스턴스가 만든 같은 문서와 바이트 단위로 동일합니다
반드시 지켜야 하는 순서에 따른 수명 주기
HotPDF가 처음부터 작성하는 모든 문서는 동일한 네 가지 단계를 거치며 이 순서는 변경할 수 없습니다. Create는 컴포넌트를 할당합니다. BeginDoc은 문서를 열고 구조적 선택을 확정하므로, 전체 파일(페이지 크기, 압축, 암호화, 출력 파일 이름)에 영향을 미치는 모든 것은 Create와 BeginDoc 사이에 설정해야 합니다. 그런 다음 그립니다. 그런 다음 EndDoc은 바이트를 디스크에 기록합니다. Free는 인스턴스를 해제합니다. BeginDoc 전에 배치된 그리기 호출은 표시될 페이지가 없으며, 그 이후에 할당된 전체 문서 속성은 경고 없이 무시됩니다. 재사용한 인스턴스에서는 FileName, Compression, PDFACompliance, Author, Title 같은 속성 설정이 다음 문서로 이어지므로, 다르게 설정해야 한다면 각 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;
작업 단위로 읽으십시오: BeginDoc 하나, EndDoc 하나, 디스크의 파일 하나. 두 번째 파일은 두 번째 BeginDoc과 EndDoc 쌍이며, 같은 인스턴스 위에서든 새 인스턴스 위에서든 마찬가지입니다
"재사용"의 의미: 하나의 인스턴스, 또는 파일당 새로운 인스턴스
절약 버전도 동작합니다: 컴포넌트를 한 번 만들고, 배치를 돌며 루프 안에서 BeginDoc과 EndDoc을 호출합니다. 각 반복은 독립적인 파일을 만들고, 한 문서를 위해 등록된 것이 다음 문서에 넘어가지 않습니다. 보호 출력에는 함정이 하나 남습니다. EndDoc은 UserPassword와 OwnerPassword를 지우므로, ActivateProtection이 켜져 있는 동안에는 다음 BeginDoc 전에 비밀번호를 다시 설정해야 합니다. 그렇지 않으면 빈 비밀번호로 파일을 보호하는 대신 예외를 일으킵니다. 아래 버전은 다른 길을 택해 각 출력을 자기만의 짧은 수명 객체로 다룹니다. 컴포넌트 생성 비용은 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이 같은 경로에 쓰려 하고, 뷰어가 작성자를 막는 읽기 공유를 쥐고 있으므로 운영체제가 거부하며, 액세스 거부 실패를 받습니다. 이것은 컴포넌트 상태 문제가 아니라 진짜 Windows 파일 잠금 문제이며, 우회법이 아니라 진짜 답을 받을 자격이 있습니다
최상위 창을 열거하고 제목이 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만큼이나 잘 맞고, 로그 줄로 거슬러 올라가는 추적성을 공짜로 얻습니다. 어느 쪽이든 원리는 같습니다: 쓰고 있는 그 파일이 쓰는 순간 여러분만의 것이라도록 설계하십시오. 잠금은 창을 억지로 닫아서가 아니라 다른 누구도 그 바이트를 만지지 않아서 사라집니다
수정의 형태
두 문제를 뿌리로 돌리면 둘 다 경계를 존중하는 문제입니다. 상태 머신 오류는 문서 경계를 지켜 달라고 합니다: 다음 문서를 시작하거나 로드하기 전에 지금의 문서를 EndDoc으로 끝내십시오. 파일 잠금 오류는 파일 경계를 지켜 달라고 합니다: 아무도 읽지 않는 곳에 쓰고, 결과를 제자리로 옮기십시오. 어느 쪽도 컴포넌트를 우회하거나 데스크톱을 스크립트로 주무를 것을 요구하지 않습니다. 둘 다 각 문서를 새로 만들고, 깨끗이 쓰고, 놓아 주는 자기완결적 작업 단위로 다루는 데서 자연히 나오며, 컴포넌트의 나머지 부분을 예측 가능하게 만드는 바로 그 패턴입니다
여기에 표시된 BeginDoc, EndDoc, LoadFromFile 및 SaveLoadedDocument 호출은 Delphi 및 C++Builder용 HotPDF Delphi Component의 일부입니다