한 청구 처리 팀은 30년 분량의 종이 파일을 자동 급지 스캐너를 통해 처리해야 했습니다. 스캐너는 페이지당 하나의 JPEG를 생성하여 0001.jpg, 0002.jpg 등의 이름으로 폴더에 저장했습니다. 아카이브에서 실제로 필요로 했던 것은 사례 파일당 하나의 PDF였으며, 페이지가 순서대로 정렬되어 검토자가 수백 개의 이미지 섬네일을 클릭하는 대신 단일 문서를 열 수 있게 하는 것이었습니다. 번호가 매겨진 스캔 더미를 단일 순서의 PDF로 변환하는 이 마지막 단계가 여기서 다룰 작업입니다
PDFium 컴포넌트는 이를 직접 처리합니다. 렌더링 및 텍스트 추출을 넘어서, 이 컴포넌트는 PDF를 처음부터 빌드할 수 있습니다: 빈 문서를 만들고, 원하는 크기의 빈 페이지를 추가하고, 해당 페이지의 사용자 공간 좌표에 이미지를 배치한 다음 저장합니다. 전체 파이프라인이 TPdf 컴포넌트에 있으므로, 일괄 변환기는 파일명에 대한 루프와 몇 번의 호출로 이루어집니다
변환의 형태
각 스캔마다 세 가지 일이 발생해야 합니다. 페이지 크기를 결정하고, 여백을 남기고 페이지 안에 이미지를 배치한 다음, 다음 페이지로 이동합니다. PDFium 컴포넌트는 각각에 대해 하나의 메서드를 제공합니다: AddPage는 지정된 크기의 빈 페이지를 생성하고, AddImage(또는 이미 TPicture를 가지고 있는 경우 AddPicture)는 비트맵을 현재 페이지에 그리며, PageNumber는 후속 그리기 호출이 대상으로 하는 페이지를 컴포넌트에 알려줍니다
사람들을 혼란스럽게 하는 한 가지 세부 사항은 좌표계입니다. PDF 사용자 공간은 페이지의 왼쪽 하단 모서리에 원점을 두고 Y가 위로 갈수록 증가하는데, 이는 Delphi 개발자가 무의식적으로 사용하는 화면 좌표와 정반대입니다. AddImage에 전달하는 X, Y는 이미지 사각형의 왼쪽 하단 모서리이며, Width, Height는 소스 파일의 픽셀 크기가 아니라 포인트 단위의 배치 크기입니다. 이를 반대로 하면 스캔본이 페이지를 벗어나거나 예상했던 위치에 비해 거꾸로 배치될 수 있습니다
문서 및 스캔당 하나의 페이지 생성하기
빈 문서로 시작합니다. CreateDocument는 새로운 PDF를 할당하고 컴포넌트를 활성 상태로 두므로 별도의 열기 단계가 필요하지 않습니다. 거기서부터 스캔한 파일 목록을 탐색하며 각각에 대해 페이지를 추가하고, 이를 현재 페이지로 만든 다음 이미지를 배치합니다. 여기서 페이지 크기는 아카이브된 서신의 표준 용지 크기인 포인트 단위의 A4(595 × 842 세로)입니다
procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
PageW = 595.0; // A4 width in points
PageH = 842.0; // A4 height in points
Margin = 36.0; // half-inch border around each scan
var
I: Integer;
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument; // new, empty, already active
for I := 0 to Files.Count - 1 do
begin
Pdf.AddPage(I + 1, PageW, PageH); // 1-based page index
Pdf.PageNumber := I + 1; // make the new page current
PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
end;
Pdf.SaveAs(OutputPath);
finally
Pdf.Free;
end;
end;
각 반복에서 페이지를 생성하고 즉시 PageNumber를 해당 페이지로 설정합니다. 이 두 번째 줄이 중요합니다: AddPage는 페이지를 삽입하지만 그리기 메서드는 현재 활성화된 페이지에 작용하므로, PageNumber를 설정해야 방금 만든 페이지에 AddImage가 지정됩니다. 이를 생략하면 이미지가 이전에 로드된 페이지에 겹쳐 쌓이게 됩니다
해당 루프에는 Files의 순서라는 한 가지 가정이 숨어 있습니다. 스캐너는 페이지 이름을 0001.jpg부터 0100.jpg까지 지정하지만, 디렉토리 열거가 항상 정렬된 상태로 반환되는 것은 아니며, page10.jpg 옆에 있는 page9.jpg를 처리하는 순간 단순한 문자열 정렬은 9페이지보다 10페이지를 먼저 배치합니다. 루프를 실행하기 전에 목록을 명시적으로 정렬하고, 어휘 순서가 페이지 순서와 일치하도록 스캔 시 0으로 채워진 이름을 선호하세요. 페이지 순서는 검토자가 가장 먼저 눈치채는 부분이며, 방지하기 가장 쉬운 실수입니다
스캔 배치 및 가로세로 비율 유지하기
스캔본이 페이지와 동일한 모양인 경우는 드뭅니다. 용지를 채우기 위해 이미지를 늘리면 텍스트가 왜곡되고, 전체 픽셀 크기로 배치하면 용지를 넘치게 됩니다. 해결책은 너비 맞춤 또는 높이 맞춤 중 더 작은 비율로 크기를 조정하고, 남은 공간을 가운데로 정렬하는 것입니다. 원점이 왼쪽 하단에 있기 때문에 가운데 정렬은 남은 공간을 균등하게 나누어 X와 Y 모두에 더하는 것을 의미합니다
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
PageW, PageH, Margin: Double);
var
Pic: TPicture;
AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
Pic := TPicture.Create;
try
Pic.LoadFromFile(FileName); // BMP, JPG, PNG, etc. via the VCL graphics units
AvailW := PageW - 2 * Margin;
AvailH := PageH - 2 * Margin;
// Fit inside the margins without distorting the scan.
Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
DrawW := Pic.Width * Scale;
DrawH := Pic.Height * Scale;
// Center: leftover space split evenly. Y measured from the page bottom.
X := (PageW - DrawW) / 2;
Y := (PageH - DrawH) / 2;
Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
finally
Pic.Free;
end;
end;
이는 픽셀 크기를 읽기 위해 파일을 한 번 로드하고, 단일한 균일한 배율을 계산한 후 배치 사각형을 AddImage에 전달합니다. AddImage는 파일 경로를 직접 허용하고 AddPicture와 동일한 이미지 파이프라인을 통해 라우팅하므로 VCL 그래픽 단위가 인식하는 모든 형식은 특별한 처리 없이 작동합니다. 미리보기 창에서 TPicture로 이미 디코딩된 이미지가 있다면, 동일한 사각형으로 AddPicture(Pic, X, Y, DrawW, DrawH)를 호출하고 두 번째 파일 읽기를 생략하세요
JPEG 스캔의 디코딩 생략하기
스캐너는 거의 항상 JPEG를 출력합니다. JPEG를 TPicture로 로드하면 비트맵으로 디코딩되고, PDFium은 저장 시 이를 다시 인코딩하는데, 이는 필요 없는 두 번의 손실 발생 과정입니다. AddJpegImage는 압축된 원본 바이트를 스트림에서 페이지로 직접 삽입하므로 대용량 일괄 처리의 경우 더 빠르고 시각 정으로 더 깔끔합니다
var
Stream: TFileStream;
begin
// ... after AddPage + PageNumber for the current page ...
Stream := TFileStream.Create(FileName, fmOpenRead);
try
// Embeds the JPEG bytes as-is; no decode/re-encode cycle.
Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
finally
Stream.Free;
end;
end;
크기를 조정하려면 픽셀 크기가 필요하므로 여전히 동일한 방법으로 X, Y, DrawW, DrawH를 계산합니다. 파일 또는 빠른 헤더 구문 분석을 통해 이를 읽은 다음 원시 스트림을 AddJpegImage에 넘깁니다. PNG 또는 TIFF 스캔의 경우 AddImage 경로가 올바르며, JPEG 단축키는 해당 형식이 실제로 적용되는 경우를 위해 남겨두세요
각 페이지 레이블 지정하기
아카이브된 스캔본은 각 페이지에 원본 파일 이름이 포함되어 있을 때 검사하기가 더 쉽습니다. AddText는 사용자 공간 좌표에 문자열을 그리므로 이미 바로 아래에 캡션이 위치합니다. 반전된 Y축을 기억하세요. 스캔 아래에 레이블을 추가하려면 이미지의 아래쪽 가장자리에 추가하는 것이 아니라 값을 빼야 합니다
// Caption below the scan: Y decreases toward the page bottom.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
X, Y - 14, clGray);
저장에 관한 마지막 한 가지 요점입니다. SaveAs는 부울(Boolean)을 반환하는 함수이므로 프로덕션 코드에서는 쓰기가 성공했다고 가정하지 말고 결과를 확인하세요. 그렇지 않으면 디스크가 꽉 차거나 잠긴 출력 경로에서 조용히 실패합니다. 루프가 끝나고 파일이 기록되면 아카이브에서 필요로 하던 것과 정확히 일치하는, 사례 파일당 하나의 정렬된 PDF, 크기가 조정된 페이지, 모든 뷰어에서 읽을 준비가 된 파일을 얻게 됩니다
동일한 구성 요소를 사용하여 관련 작업을 다룰 수 있습니다. 페이지별 크기 조정 규칙을 바꾸면 시트당 하나의 이미지가 있는 사진첩을 얻을 수 있고, 루프를 유지하면서 TIFF 다중 페이지 소스에서 읽으면 팩스 아카이브 변환기가 됩니다. 프로그래밍 방식으로 PDF를 빌드하는 더 넓은 그림을 보려면 PDFium 컴포넌트를 사용하여 처음부터 PDF 문서 만들기를 참조하고, 나중에 화면으로 결과를 다시 렌더링하려면 PDFium 컴포넌트를 사용하여 PDF 페이지를 JPEG 이미지로 변환하기를 참조하세요
loslab.com의 PDFium 컴포넌트는 이 시리즈 전체에서 사용되는 문서 생성, 렌더링 및 텍스트 API를 번들로 제공합니다