압축 기능이 출시된 주에 두 가지 불만이 도착합니다. 스캔한 계약서가 이제 계단 모양의 털덜덜한 글자 모양을 갖고, 표지 페이지의 투명 로고가 창백한 헤일로 안에 앉아 있습니다. PDFiumPas는 둘 다 한 곳에서 답합니다. TPdf.OptimizeImages는 줄이기 전에 각 이미지를 측정한 뒤, 리샘플링 커널을 고르고 색을 알파 인식 형태로 누적합니다
그렇게 항상 그랬던 것은 아닙니다. v3.100.0 이전 같은 메서드는 모든 비-바이레벨 이미지를 고정된 최근접 이웃 단계로 다운샘플했는데, 그것이 정확히 두 불만을 만드는 알고리즘입니다. 출력 픽셀마다 소스 픽셀 하나를 점샘플하고, 완전 투명 픽셀 아래 앉은 RGB를 독자가 볼 일이 있는 것처럼 다룹니다. v3.100.0의 재작성은 그 단일 경로를 다섯 커널과 측정된 선택 규칙과 명시적 작업 메모리 예산으로 바꿉니다
다운샘플링은 왜 스캔 텍스트를 거칠게 만드는가
점샘플링이 잘못된 질문에 답하기 때문입니다. 300 DPI 스캔을 150 DPI로 재목표하면 모든 목적지 픽셀이 2×2 소스 픽셀 블록을 대표하고, 최근접 이웃은 넷 중 하나를 쥐고 나머지를 버립니다. 어느 하나가 살아남는지는 반올림에 달려 있으므로, 소스에서 매끄럽게 앤티앨리어스된 획 가장자리는 픽셀마다 동전 던지기가 됩니다. 결과는 글리프 가장자리를 따라 고전적인 앨리어스 계단이고, 버려진 샘플이 우연히 패턴을 실었던 하프톤 영역에는 모아레가 얹힙니다. 이것이 화면보다 PDF에서 더 중요한 이유는 피해가 영구적이기 때문입니다. 이미지 XObject는 샘플 데이터를 /Width, /Height, /BitsPerComponent와 함께 싣고(ISO 32000-1 §8.9.5), 그리고 리샘플링은 파일 안에서 셋 모두를 재작성합니다. 뷰어에서 나쁜 줌은 다시 그릴 수 있는 프레임이고, PDFiumPas에는 그것을 위한 별도 기계가 렌더 캐시와 줌 성능에 있습니다. 나쁜 다운샘플은 고객에게 건네는 새 문서입니다
PDFiumPas가 디테일을 측정하고 커널을 고르는 방법
PDFiumPas는 문서 단위가 아니라 이미지 단위로 결정합니다. 커널을 고르기 전에 유계 샘플링 격자에서 정규화된 휘도 디테일 점수를 계산합니다. 수평과 수직 단계는 (Width + 63) div 64와 (Height + 63) div 64이므로, 12000픽셀 스캔과 300픽셀 썸네일이 둘 다 거의 같은 64×64 훑기를 씁니다. 샘플 위치마다 오른쪽 이웃과 아래 이웃에 대한 절대 차이를 최대 세 채널에 걸쳐 합한 뒤, 샘플 개수 곱하기 255로 나눕니다. 점수는 0부터 1에 착지하고, 평평한 비즈니스 그래픽은 0 근처에 앉고 빽빽한 사진 텍스처는 올라갑니다
선택 사다리는 고정된 순서로 돕니다. ResampleFilter가 pirfAdaptive가 아닌 무엇이면 그 필터가 그대로 쓰입니다. 그렇지 않으면: 1비트 콘텐츠는 pirfBilevel을, piccLineArt의 ContentClass는 pirfBox를, 4 이상의 스케일 인자도 pirfBox를 취합니다. 그 축소에서 영역 평균이 가장 싸고 가장 올바른 답이기 때문입니다. piccPhoto, 0.08 이상의 디테일 점수, 또는 0.9 이상의 PreferredQuality는 세 로브 커널의 pirfLanczos를 취합니다. 2 이상의 스케일이나 0.7 이상의 품질은 반지름 2의 pirfBicubic을 취하고, 남은 전부는 pirfBilinear를 취합니다. TPdfImageOptimizeOptions.Default는 PreferredQuality를 0.85로 두므로, 기본 실행은 축소가 온화하고 콘텐츠가 평평하지 않은 한 결코 바이리니어로 떨어지지 않습니다
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// 기본값: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, 품질 0.85, 64 MiB 예산.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
이미지는 수평과 수직 배치 DPI 중 큰 쪽을 TargetDpi로 나눈 값이 MinDpiRatio에 도달할 때만 만져집니다. 그 가드가 존재하는 이유는 150 DPI 목표를 겨냥한 160 DPI 사진이 품질 한 세대를 갈아 넣고 6퍼센트 얻는 재인코딩을 당하지 않게 하기 위해서입니다. 어느 축에서든 MinDimension 아래 이미지, 기본 8, 는 아이콘이나 선으로 취급해 건너뜁니다
투명 로고는 왜 흰 테를 얻는가
완전 투명 픽셀 아래의 색은 임의적이고, 평범한 가중 평균은 그것에게 표를 주기 때문입니다. 디자인 도구에서 로고를 내보내면 보이지 않는 여백은 흔히 흰색이거나 검은색이거나 캔버스가 무엇이든 그것입니다. 알파 채널이 그것을 숨기고, 커널 발자국 위의 직선 합이 그것을 재빨리 보이는 가장자리에 다시 섞습니다. PDFiumPas는 BGRA 샘플을 프리멀티플라이드 형태로 누적하고 목적지 픽셀에서만 프리멀티플라이를 되돌려 이것을 피합니다
구체적으로, 기여하는 각 샘플은 색 누적기에 channel * alpha * weight를, 알파 누적기에 alpha * weight를, 가중치 합에 weight를 더합니다. 목적지 색은 가중치 합이 아니라 알파 누적기로 나뉘고, 그것이 중요한 단계입니다. 가중치 합으로 나누면 색이 보이지 않는 픽셀 쪽으로 끌려가고, 누적된 알파로 나누면 보이는 샘플들이 실제로 합의한 색이 재구성됩니다. 목적지 알파는 별도의 양, 255 * AlphaSum / WeightSum입니다. 알파 없는 포맷은 평소처럼 가중치 합으로 나누고, FPDFBitmap_BGRx 목적지의 패딩 바이트는 상수 255로 쓰이며, 모든 채널은 저장되기 전에 0부터 255로 클램프됩니다. 그 알파는 평소 이미지 사전의 소프트 마스크 항목(ISO 32000-1 §11.4)에서 나오는데, PDFium이 이미 리샘플러가 받는 BGRA 버퍼로 합성해 둡니다
// 안쪽 누적 루프의 모양, 기여하는 소스 샘플마다
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... 그리고 목적지 픽셀에서, 알파 합에 대해 언프리멀티플라이
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
1비트 라인 아트를 회색 지대 밖에 두기
바이레벨 스캔에 적용된 연속 커널은 무엇이든 회색을 만들고, 회색은 팩스 스타일 이미지가 담는 것이 허락되지 않은 바로 그것입니다. 그래서 PDFiumPas는 1비트 이미지를 기본으로 그대로 둡니다. PreserveBilevel은 TPdfImageOptimizeOptions.Default에서 True이고, 그런 이미지는 SkippedCount에 손대지 않은 채 착지합니다. False로 두면 스무딩 커널 대신 pirfBilevel 경로가 인수합니다. 그것은 각 목적지 픽셀을 덮는 정확한 소스 사각형을 걷고, BGR 메모리 순서의 0.114, 0.587, 0.299 가중치로 휘도를 평균하고, 결과를 127.5에서 문턱 잡아 평평한 0이나 255로 만듭니다. 중간 값은 쓰일 수 없으므로 가장자리는 또렷하게 유지되고 가는 획 주위에 회색 헤일로가 형성되지 않습니다. BGRA 소스의 알파 채널은 평소처럼 평균되고, BGRx 목적지는 상수 255를 받습니다. 더 작은 문서가 아니라 밑의 픽셀이 필요하다면 PDF 문서에서 이미지 추출이 별도 경로입니다
이미지가 작업 메모리 예산을 넘으면 무슨 일이 벌어지는가
정확히 그대로 남겨지고, 세어 집니다. MaxWorkingBytes는 기본 64 MiB이고 두 번 집행됩니다. 목적지 비트맵이 만들어지기 전에, PDFiumPas는 너비 곱하기 높이 곱하기 픽셀당 바이트가 예산을 넘으면 이미지를 거절합니다. FPDFBitmap_CreateEx 성공 뒤에는 실제 스트라이드 곱하기 높이로 다시 검사합니다. 행 패딩이 소박한 곱이 통과한 한계를 넘어서 할당을 밀 수 있기 때문입니다. 어느 거절이든 목적지를 파괴하고 아무것도 돌려주지 않습니다. 이것이 함축하는 저하를 똑똑히 보십시오. 예산 초과 이미지는 더 낮은 품질로 리샘플되지 않고, 타일로 쪼개지지도 않습니다. 원본은 문서에 남고, BudgetExceededCount와 SkippedCount는 둘 다 증가하며, 그래서 한 실행은 문서가 부분적으로만 최적화된 채로 성공을 보고할 수 있습니다. 일부러 한 fail-safe 행동이지만, 보고서가 선택 독서가 아니라는 뜻입니다. 뚜렷이 다른 실패 양상도 있습니다. PDFium이 비트맵을 아예 만들 수 없는 이미지, CMYK, JPX, JBIG2, 마스크된 소스 같은 것은 대신 FailedCount를 올리고 마찬가지로 손대지 않은 채 남습니다
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // 바이레벨 영역 투표를 쓴다
Options.ContentClass := piccPhoto; // 사진 세트에 Lanczos 강제
Options.MaxWorkingBytes := 256 * 1024 * 1024; // 큰 스캔을 위한 여유
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
파일을 출고하기 전에 보고서 읽기
TPdfImageOptimizeReport는 단지 로그되는 것이 아니라 진단되기 위해 만들어졌습니다. OptimizedCount, SkippedCount, FailedCount 곁에 커널마다 카운터 하나를 노출하므로, BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount, BilevelFilterCount가 적응 규칙이 여러분의 말뭉치에 대해 실제로 결론낸 것을 말해 줍니다. 전부 box 결과는 축소가 가팔랐거나 콘텐츠가 라인 아트로 분류되었다는 뜻이고, 라인 아트라고 믿은 문서에서 전부 Lanczos 결과는 ContentClass를 명시적으로 두어야 한다는 신호입니다. AverageDetailScore는 PreferredQuality를 조정할 때 0.08 Lanczos 문턱과 비교할 숫자이고, PeakWorkingBytes는 실행이 MaxWorkingBytes 중 얼마를 정말로 필요로 했는지 보여 줍니다. 잘못된 옵션은 조용히가 아니라 요란하게 실패합니다. 양수가 아닌 TargetDpi, 1 미만의 MinDpiRatio, 0부터 1을 벗어난 PreferredQuality, 양수가 아닌 MaxWorkingBytes는 어떤 페이지도 만져지기 전에 EPdfError를 던집니다. 그리고 OptimizeImages는 메모리 문서만 편집합니다. 수정된 각 페이지는 FPDFPage_GenerateContent로 커밋되고, 그 뒤에도 여러분은 SaveAs를 직접 부릅니다. 무엇이 바뀌었는지 눈으로 보려면 PDF 페이지를 JPEG 이미지로 변환에서 기술한 대로 이전과 이후 문서를 비트맵으로 렌더해 최대 줌에서 비교하십시오
적응 리샘플링은 동작할 때 보이지 않고 동작하지 않을 때 지원 티켓을 만들어 내는 기능 중 하나입니다. 그래서 측정, 알파 처리, 메모리 예산이 세 개의 별도 개선이 아니라 함께 착지해야 했습니다. Delphi, C++Builder 또는 Lazarus 제품용으로 평가 중이라면, 전체 API 표면과 라이선싱 세부는 PDFiumPas Delphi PDFium component page에 있습니다