PDF는 이미지를 콘텐츠 스트림 내부에 일급 객체로 저장합니다. 페이지가 사진, 스캔 또는 다이어그램을 참조할 때 픽셀 데이터는 페이지 기하학과 함께 XObject 딕셔너리에 있습니다. PDFium 컴포넌트는 TPdf의 두 가지 속성(현재 페이지에 임베디드 비트맵이 몇 개 있는지 반환하는 BitmapCount와 이 중 하나를 사용자가 소유하고 해제해야 하는 TBitmap으로 디코딩하는 Bitmap[Index])을 통해 이 표면을 드러냅니다. 이것이 추출 모델의 전부입니다. 루프는 4줄입니다. 판단이 필요한 것은 주변부의 배관(plumbing) 작업입니다
문서 열기
TPdf에 대해 가장 먼저 알아야 할 것은 Active := True가 결코 발생(raises)하지 않는다는 것입니다. 로드 실패, 잘못된 비밀번호, 손상된 파일: 모두 내부적으로 삼켜지고 컴포넌트는 단순히 비활성 상태를 유지합니다. 할당 후 직접 플래그를 확인하지 않으면 PageCount가 0을 반환하는 페이지 루프로 진행되어 아무것도 추출되지 않는 이유를 궁금해하게 될 것입니다
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
Writeln('Failed to open: ', Pdf.FileName);
Exit;
end;
Writeln(Pdf.PageCount, ' pages');
// proceed to extraction
finally
Pdf.Free;
end;
end;
비밀번호로 보호된 파일도 동일한 패턴을 따릅니다: Active := True를 설정하기 전에 Pdf.Password를 할당합니다. 비밀번호가 틀리면 Active는 False로 유지되고 여러분이 잡아낼 수 있는 예외를 얻을 수 없습니다. 수백 개의 파일을 처리하는 일괄(batch) 처리 도구에서 이러한 침묵 동작은 사실 유용합니다: 각각의 호출 스택을 푸는 대신 실패를 목록에 축적하기 때문입니다
페이지 반복 및 비트맵 가져오기
BitmapCount는 페이지당 기준이므로, 읽기 전에 Pdf.PageNumber를 설정합니다. 페이지 번호는 1 기반(1-based)이며, 기본값은 0으로 설정되어 있어 아무 페이지도 로드되지 않았음을 의미합니다. Bitmap[Index] 속성은 0 기반(0-based)이며, 호출자가 소유하는 TBitmap을 반환합니다. 이를 반드시 해제해야 합니다. 방대한 문서에 대한 긴 루프 내부에서 해제를 소홀히 하면 메모리가 기하급수적으로 증가할 수 있습니다. 각 비트맵이 압축 전에는 수 메가바이트의 로우 픽셀 데이터일 수 있기 때문입니다
procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
Page, Idx: Integer;
Bmp: TBitmap;
OutPath: string;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
for Idx := 0 to Pdf.BitmapCount - 1 do
begin
Bmp := Pdf.Bitmap[Idx];
if not Assigned(Bmp) then
Continue;
try
OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
Bmp.SaveToFile(OutPath);
finally
Bmp.Free;
end;
end;
end;
end;
Assigned 가드는 중요합니다. 소수의 PDF 생성기는 픽셀 크기가 0이거나 기타 잘못된 데이터 형식의 이미지 XObject를 쓰는데, 이러한 경우 컴포넌트는 빈 비트맵 대신 nil을 반환합니다. nil 반환을 오류로 취급하고 추출을 중단하는 것은 잘못된 반사 작용입니다: 그것을 건너뛰고, 감사 추적을 위해 페이지와 인덱스가 필요한 경우 로깅하고, 계속 진행하십시오. 페이지의 나머지 부분에서 유효한 이미지를 계속 제공할 수도 있습니다
외부 루프가 매 반복마다 Pdf.PageNumber를 설정한다는 점을 주목하십시오. 이 할당은 컴포넌트의 내부 상태에 페이지를 로드하고 BitmapCount를 유의미하게 만드는 역할을 합니다. 이를 생략하면 동일한 페이지의 개수를 계속 반복해서 읽게 됩니다. 이 패턴을 작성할 때 중복되게 느껴질 수 있지만, 이것이 API 설계 방식입니다: 페이지는 컬렉션이 아니라 커서(cursor)입니다
출력 포맷 선택하기
BMP는 무손실이며 별도의 유닛 없이 항상 사용할 수 있기 때문에, 이미지의 내용을 아직 알 수 없을 때 훌륭한 기본값이 됩니다. 파일 크기가 중요한 경우, 반환되는 TBitmap의 픽셀 포맷은 어떤 코덱이 적절한지 알려줍니다. 32비트 비트맵은 알파 채널을 가지며, PNG는 손실 없이 이를 보존합니다. 연속적인 톤을 가진 크기가 큰 24비트 이미지는 JPEG에 적합한 후보입니다. 작은 이미지이거나 제한된 팔레트로 그려진 이미지는 대체로 JPEG 처리보다 BMP로 남겨두는 것이 더 나은데, JPEG는 낮은 화질 설정에서 블록 결함(blocking artifacts)을 추가하고, 높은 화질 설정에서도 저장 효율이 거의 없기 때문입니다
procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
Jpg: TJPEGImage;
begin
case UpperCase(ExtractFileExt(FileName)) of
'.JPG', '.JPEG':
begin
Jpg := TJPEGImage.Create;
try
Jpg.Assign(Bmp);
Jpg.CompressionQuality := 85;
Jpg.SaveToFile(FileName);
finally
Jpg.Free;
end;
end;
else
Bmp.SaveToFile(FileName); // BMP: lossless, no extra units
end;
end;
실제 환경에서 포맷 선택은 Bmp.PixelFormat 및 치수에 따라 결정됩니다. PixelFormat = pf32bit 인 경우 알파 채널을 전달할 수 있는 포맷이 필요하므로 PNG가 확실한 선택이지만, 이전 델파이 버전에서는 PNGImage 단위가 필요합니다. 약 300 픽셀보다 넓은 24비트 이미지의 경우 85 화질의 JPEG는 3:1 크기 감소를 제공하며, 대부분의 사진 콘텐츠에서 인식할 수 있는 화질 손실이 발생하지 않습니다. 이 임곗값 미만에서는 BMP의 크기가 비슷하며 화질 결정 자체가 전혀 필요 없습니다
BitmapCount가 계산하는 것과 계산하지 않는 것
PDF는 이미지 XObject와 경로(path) 연산자로 그려진 벡터 그래픽을 구분합니다. 모든 요소가 벡터인 경우, 시각적으로 복잡해 보이는 페이지라도 BitmapCount가 0을 반환할 수 있습니다. 스캔된 페이지는 거의 항상 정확히 하나를 반환합니다: 스캐너는 스캐너에 설정된 해상도에 상관없이, 전체 스캔을 단일 전체 페이지 이미지 XObject로 기록합니다. 식자된 텍스트와 삽입된 사진이 혼합된 페이지는 사진 하나당 한 개의 항목을 반환합니다. 장식선, 음영 처리된 배경, 테이블 테두리 등은 일반적으로 비트맵 수에 전혀 나타나지 않습니다
이 계산에는 거의 사용되지 않는 PDF 구조인 인라인(inline) 이미지도 포함되지 않는데, 이는 이미지 데이터가 네임드(named) XObject 형태가 아니라, 페이지 콘텐츠 스트림 안에 직접 임베디드되는 경우를 뜻합니다. 이는 이 API가 제공하는 표면 밖에 있으며, 실제 문서에서는 매우 흔치 않아 대부분의 추출 툴은 이를 단순 처리하지 않습니다
한 가지 주의해야 할 사항: 읽어온 BitmapCount는 마지막으로 설정된 PageNumber 시점 기준의 현재 페이지를 위한 것입니다. 카운팅과 페칭 사이에 PageNumber를 변경하는 함수를 코드가 분기하거나 호출하게 되면, 할당했던 공간보다 적은 수의 이미지를 읽게 되거나 인덱스가 끝을 지나갈 수 있습니다. BitmapCount 읽기와 Bitmap[] 루프는 동일한 페이지에 유지하고, 중간에 PageNumber를 건드리지 않도록 하십시오
폼(form) 애플리케이션에서 TPdfView 사용하기
TPdfView 컴포넌트는 TPdf.PageNumber가 아닌 뷰에 현재 표시된 페이지에서 BitmapCount 및 Bitmap[] 속성을 노출합니다. 이 두 페이지 포인터는 독립적이며 하나를 설정해도 다른 쪽은 이동하지 않습니다. 라이브 뷰어가 있는 VCL 폼 애플리케이션에서는 Pdf.PageNumber := N을 호출하여 사용자가 마지막으로 스크롤한 페이지에 뷰어가 머무는 동안 TPdf를 통한 추출을 구동할 수 있습니다. 이러한 분리는 의도된 것이며, 백그라운드 추출이 실행되는 동안 뷰어의 디스플레이 상태를 깨끗하게 유지합니다
일괄 처리 작업에서의 메모리와 성능
방대한 아카이브 전체에 걸쳐 살펴봐야 할 주요 사항은 메모리 예산입니다. 각 Bitmap[] 호출은 힙(heap) 공간에 새 TBitmap을 할당하며, 300 DPI로 스캔한 페이지의 경우 이는 픽셀 압축 해제 전 대략 25MB의 비가공 데이터 크기에 달할 수 있습니다. 반복 사이사이에 메모리를 해제하지 않고 빡빡한 루프 내에서 여러 페이지를 처리한다면, 작업 세트가 비트맵의 개수에 비례하여 선형적으로 증가하게 됩니다. 올바른 형태는 언제나 '비트맵을 한 개 페치하고, 필요한 작업을 수행하고, 해제한 다음, 다음 비트맵을 페치하는' 식입니다. 비교 작업을 위해 여러 비트맵을 동시에 보유해야 하는 경우라면 BitmapCount로 그 수를 먼저 계산하고 알맞게 컨테이너를 할당한 뒤, 문서 처리 마지막에 몰아서 메모리를 정리하지 말고 각각의 비트맵 사용을 마치는 즉시 해제하십시오. 500개의 스캔 페이지가 있는 문서의 경우 이러한 차이가 최고 25MB와 12GB의 RSS(Resident Set Size) 차이로 나타날 수 있습니다
여기에 표시된 BitmapCount 및 Bitmap[] 속성은 델파이 및 C++Builder용 PDFium 컴포넌트의 일부입니다