Delphi용 PDFium Component에서 TPdfLibraryConfiguration의 BrotliEnabled나 IsolatePerDocument를 켜면 번들된 Skia 빌드가 오류 없이 AGG 렌더러로 바뀌곤 했습니다. 두 옵션 모두 PDFium이 m_RendererType을 문자 그대로 읽는 버전으로 FPDF_LIBRARY_CONFIG를 올리기 때문입니다. v3.123.0부터 기본 렌더러는 DLL 자체의 기본값에 머물고, v3.125.0부터 DLL이 이행할 수 없는 Skia나 Fontations 요청은 프로세스를 죽이는 대신 잡을 수 있는 EPdfError를 일으킵니다
두 버그 모두 자신을 알리지 않았습니다. 첫째는 멀쩡해 보이는 페이지를 만들었습니다. 출시하고 테스트한 빌드와 앤티 앨리어싱과 텍스트 가장자리가 살짝 다른, 그저 다른 래스터라이저가 렌더링한 페이지요. 둘째는 알렸습니다. 시끄럽게, 네이티브 초기화 안쪽에서 호스트 프로세스를 끌어내리면서요. 둘 다 같은 곳에서 나옵니다. 버전 번호가 말하기 전까지는 필드가 아무 의미도 없고, 0 값이 "설정 없음"이 아니라 진짜 선택인 버전화된 C 구조체입니다
FPDF_LIBRARY_CONFIG는 PDFium이 어떤 렌더러를 쓸지 어떻게 정할까?
FPDF_InitLibraryWithConfig는 구조체의 Version 필드가 4 이상일 때만 m_RendererType을 참조하며, 그 버전부터는 쓰인 값 그대로 씁니다. version 4 미만에서 PDFium은 필드를 무시하고 빌드 기본값을 고릅니다. PDF_USE_SKIA로 컴파일된 빌드에서는 Skia, 그 외에서는 AGG입니다
이후의 각 필드는 같은 패턴을 따릅니다. 구조체는 한 번에 기능 하나씩 자랐고, 각 기능은 새 버전 번호와 함께 도착했습니다. PDFium Component는 LoadLibrary에서 여러분의 TPdfLibraryConfiguration으로 네이티브 구조체를 만들며, 설정한 옵션이 요구하는 만큼만 버전을 올립니다
| 구조체 버전 | 추가하는 필드 | 설정 주체 |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | 언제나 기록됨, V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform이 nil이 아님 |
| 4 | m_RendererType | prpDefault 이외의 Renderer |
| 5 | m_FontLibraryType | pfbpDefault 이외의 FontBackend |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
함정은 마지막 두 행에 있습니다. 버전은 누적입니다. version 6 구조체는 version 4이자 version 5 구조체이기도 하므로, Brotli만 요청했어도 PDFium은 m_RendererType과 m_FontLibraryType을 읽습니다. 그 순간 두 필드에 앉아 있는 무엇이든 여러분이 고를 뜻이 있었든 없든 렌더러와 폰트 백엔드가 됩니다
Brotli를 켜면 왜 렌더러가 AGG로 바뀌었을까?
v3.123.0 전의 PDFium Component는 prpDefault에 대해 m_RendererType에 FPDF_RENDERERTYPE_AGG를 썼으므로, 구조체를 version 6이나 7로 밀어 올린 설정은 무엇이든 Skia 빌드에 AGG를 강요했습니다. 컴포넌트와 함께 배송되는 pdfium.dll과 pdfium.v8.dll 런타임은 Skia 빌드이므로, 이것은 별난 배포가 아니라 기본 배포를 때렸습니다
그 사상은 쓰일 때는 무해해 보였습니다. version 2나 3에서는 필드를 결코 읽지 않으므로 prpDefault는 정말로 "DLL이 하는 무엇이든"을 뜻했습니다. BrotliEnabled(version 6)나 IsolatePerDocument(version 7)가 그림에 들어오는 순간 같은 코드가 "선호 없음"을 명시적인 AGG 요청으로 바꿨습니다. 실패한 것은 없습니다. PDFium은 평범하게 초기화하고 모든 페이지를 렌더링하고 오류 코드를 돌려주지 않았습니다. 그 입장에서는 호출자가 AGG를 요청했고 AGG를 받았으니까요
픽셀 해시는 스크린샷이 보여 주지 못하는 곳에서 바뀜을 보여 줍니다. 같은 샘플 문서의 첫 페이지를 세 가지 설정 아래에서 렌더링한 결과입니다:
- 기본 설정: 해시
502D77C3711B4ACF BrotliEnabled= True에Renderer는prpDefault로 둠: 해시F75B5EB4728ADE87- 명시적
prpAgg: 해시F75B5EB4728ADE87, Brotli 실행과 동일
v3.123.0의 수정은 공개 함수 PdfNativeRendererType입니다. TPdfRendererPreference를 m_RendererType에 쓰이는 값으로 해석합니다. prpAgg와 prpSkia는 일대일로 사상됩니다. prpDefault는 이제 로드된 DLL이 FPDF_RenderPageSkia를 export하면 Skia로, 아니면 AGG로 사상됩니다. 그 export는 Skia 기본값 자체와 같은 PDF_USE_SKIA 조건 아래에서 컴파일되므로, DLL 바깥에서 관찰할 수 있는 유일한 빌드 속성입니다. 수정 뒤 Brotli 설정은 기본 설정과 같은 해시를 만들어 냅니다
폰트 백엔드에는 같은 문제가 없었습니다. m_FontLibraryType은 version 5부터 읽히며, 그 0 값인 FPDF_FONTBACKENDTYPE_FREETYPE는 필드를 전혀 읽지 않을 때의 PDFium 기본값이기도 합니다. pfbpDefault에 FreeType을 쓰면 따라서 네이티브 기본값을 정확히 재현합니다. 0 값이 언제나 틀린 건 아니고, 그저 자동으로 맞는 일은 결코 없을 뿐입니다
v3.123.0 이상에서는 자연스럽게 쓰게 될 시작 코드가 이제 하는 말대로 동작합니다:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// 무엇이든 네이티브 라이브러리를 로드하기 전에 실행해야 함
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // FPDF_LIBRARY_CONFIG를 version 6으로 올림
// Renderer는 prpDefault 유지: FPDF_RenderPageSkia를 export하는 빌드에서는
// Skia로, AGG 전용 빌드에서는 AGG로 해석됨
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
BrotliEnabled는 DLL 자체가 PDF_ENABLE_BROTLI로 빌드됐을 때에만 PDF 2.0의 /BrotliDecode 스트림을 디코딩 가능하게 만든다는 점을 기억하세요. 플래그는 요청이며, Brotli 지원 없는 빌드에서는 효과가 없습니다. TPdfLibraryConfiguration.Hardened는 AllowMachineTime이 False라는 점만 빼면 Default와 같은데, 그것은 문서 JavaScript가 진짜 시계를 읽지 못하게 막습니다. 신뢰할 수 없는 파일의 서버 측 처리에는 합리적인 출발점입니다
DLL이 담고 있지 않은 백엔드를 요청하면 무엇이 일어날까?
PDFium은 빌드에 없는 렌더러나 폰트 백엔드에 대해 오류를 돌려주지 않습니다. FPDF_InitLibraryWithConfig는 네이티브 CHECK를 실패시키는데, Windows에서는 breakpoint 예외로 표면화되고 호출 주위에 구조화 예외 핸들러가 없으면 프로세스를 끝냅니다. 헤더도 그렇게 경고합니다. 지원되지 않는 값은 "마찬가지로 즉각적인 크래시로 실패할 것"이라고요
구체적인 두 사례는 FPDF_RENDERERTYPE_SKIA를 받는 AGG 전용 빌드와, Fontations 없는 빌드가 받는 FPDF_FONTBACKENDTYPE_FONTATIONS입니다. 번들된 Skia 런타임은 두 번째 그룹입니다. Skia로 렌더링하지만 폰트에는 FreeType을 씁니다. 그것에 대해 prpSkia와 pfbpFontations를 함께 요청하면 Delphi 쪽에서 External exception 80000003이 나왔습니다. 디버거나 예외 핸들러가 우연히 그것을 잡더라도 상황은 여전히 복구 불가입니다:
- PDFium은 반쯤 초기화된 채로 남습니다
- 프로세스 전역 설정은 이미 봉인됐으므로,
ConfigurePdfLibrary는 고쳐진 설정을 거부합니다 - 같은 프로세스에서 다른 설정으로 재시도하는 것은 더 이상 불가능합니다
이것은 Brotli 버그와 정반대의 실패입니다. 저쪽에서는 필드가 아무도 고르지 않은 값을 지녔고 PDFium이 조용히 받아들였습니다. 이쪽에서는 필드가 호출자가 의도적으로 고른 값을 지니고 PDFium은 그것에 대한 어떤 논의도 받지 않습니다. 둘 다 래퍼가 네이티브 호출 전에 풀어야 할 문제입니다. 호출 뒤에는 잡을 것이 하나도 남지 않으니까요
PDFium Component는 Skia와 Fontations를 어떻게 사전 검사하나
v3.125.0부터 LoadLibrary는 DLL export를 바인딩한 뒤, FPDF_InitLibraryWithConfig를 호출하기 전에 설정을 검증하고, 지원되지 않는 렌더러나 폰트 백엔드를 문제의 설정과 대안을 이름으로 말하는 EPdfError로 바꿉니다. DLL은 언로드되고 설정은 봉인 해제되므로, 호출자는 다른 설정을 고르고 다시 로드할 수 있습니다
결정 자체는 순수 함수 PdfLibraryConfigurationSupportError에 삽니다. 설정과 빌드를 기술하는 boolean 둘을 받아 조합이 안전하면 빈 문자열을 돌려줍니다. 네이티브 상태를 만지지 않으므로, 자기 테스트에서 어떤 기능 조합으로든 호출할 수 있습니다. LoadLibrary 안에서 두 boolean은 다른 종류의 증거에서 나오며, 서로 다른 신뢰 수준을 받을 자격이 있습니다:
- Skia는
FPDF_RenderPageSkiaexport의 존재에서 탐지됩니다.PdfNativeRendererType이 쓰는 바로 그 신호입니다. export와 Skia 렌더러는 하나의 조건 아래에서 컴파일되므로 검사는 정확합니다 - Fontations에는 자기 export가 없습니다. 남기는 흔적은 바이너리로 끌어들이는 Rust 폰트 크레이트뿐이므로, PDFium Component는 로드된 라이브러리 파일에서 크레이트 이름
skrifa와read-fonts(read_fonts도)를 스캔합니다. 스캔은pfbpFontations가 요청될 때만 돌고, 읽을 수 없는 파일은 "Fontations 없음"으로 셉니다
Fontations 검사는 휴리스틱이며 한 방향으로 틀릴 수 있습니다. 그 문자열들을 전부 떼어 낸 Fontations 빌드는 동작했을 텐데도 거부됩니다. 그 트레이드는 일부러 한 것입니다. 거짓 거부는 잡을 수 있는 예외 하나와 FreeType 폴백을 대가로 청구합니다. 거짓 수용은 프로세스를 대가로 청구합니다
봉인 해제는 검사만큼 중요합니다. LoadLibrary는 로드의 아주 시작에서 설정을 봉인하므로, 리셋이 없다면 기능 거절 뒤에 ConfigurePdfLibrary는 모든 재시도에 EPdfError "PDFium library configuration is already sealed"로 답할 것입니다. 거절 경로는 먼저 UnloadLibrary를 호출합니다. 그 FPDF_DestroyLibrary 호출은 그 시점에서 안전합니다. PDFium이 아직 초기화되지 않아 즉시 돌아오기 때문입니다. DLL 누락이나 아키텍처 불일치 같은 다른 로드 실패는 봉인을 유지하므로, 재시도 루프는 둘을 구별해야 합니다:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // 유닛 한정: Windows.LoadLibrary가 같은 이름
Result := prpSkia;
except
on E: EPdfError do
begin
// 기능 거절은 DLL을 언로드하고 설정의 봉인을 해제함.
// 아예 로드에 실패한 DLL은 sealed 유지: 재시도는 도움이 안 됨
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
명시적인 PDFium.LoadLibrary에 주목하세요. Windows나 Winapi.Windows도 쓰는 유닛에서 한정 없는 LoadLibrary는 uses 절에 마지막으로 나오는 유닛으로 해석됩니다. 그것이 Win32 함수라면 인자 없는 호출은 PDFium에 대해 아무것도 말하지 않는 인자 개수 오류로 컴파일에 실패합니다
더 이른 시점에 일어나는 검증
ConfigurePdfLibrary는 어떤 DLL도 관여하기 전에 몇몇 조합을 거부하며, 모두 EPdfError입니다. pfbpFreeType을 포함한 명시적 FontBackend는 Renderer = prpSkia를 요구합니다. PDFium은 폰트 백엔드를 Skia 렌더러에 대해서만 참조하기 때문입니다. IsolatePerDocument는 V8Isolate가 nil이기를 요구합니다. PDFium은 문서별로 자기 isolate를 만들며, 여러분이 하나를 더 건네주면 네이티브 CHECK를 실패시킵니다. UserFontPaths의 빈 문자열은 거부됩니다. 그리고 첫 로드 시도 뒤의 어떤 호출이든 "PDFium library configuration is already sealed"로 실패합니다
마지막 규칙에는 실용적 귀결이 있습니다. 먼저 DLL을 들여다보고 나중에 설정할 수는 없습니다. GetSkiaRenderCapabilities, V8FeaturesAvailable, 문서 열기, 그 밖의 대부분 진입점은 내부적으로 LoadLibrary를 호출하며, 그것은 그 자리에서 설정을 봉인합니다. 나중에 UnloadLibrary를 호출해도 다시 열리지 않습니다. 먼저 설정하고, 그다음 로드하고, 그다음 질문하세요. 진단 루틴이 따라야 할 정확히 그 순서입니다:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // 사본, 들여다보기 안전
if not PDFium.Loaded then
begin
if PdfLibraryConfigurationSealed then
Exit('PDFium failed to load; configuration is sealed');
Exit('PDFium not loaded; configuration can still change');
end;
// FPDF_LIBRARY_CONFIG를 만들 때 LoadLibrary가 적용한 것과 같은 해석
if PdfNativeRendererType(Config.Renderer,
GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
Renderer := 'Skia'
else
Renderer := 'AGG';
Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
[Renderer, BoolToStr(Config.BrotliEnabled, True),
BoolToStr(Config.IsolatePerDocument, True)]);
end;
그 줄을 시작 때 한 번 기록하는 것은 값싸고, "서버에서 텍스트가 다르게 보인다"는 지원 티켓에서 가장 먼저 원하는 것입니다. PDFium.Loaded가 한정된 이유는 LoadLibrary와 같습니다. 폼이나 컴포넌트 메서드 안에서 맨 Loaded는 TComponent.Loaded에 바인딩됩니다
버전화된 C 설정 구조체가 잘못되는 두 가지 방식
버전화된 설정 구조체는 FPDF_LIBRARY_CONFIG든, Win32 cbSize 레코드든, 플러그인 ABI든, 대칭적인 두 가지 방식으로 실패하며 래퍼는 둘 다 막아야 합니다. 첫째는 버전을 너무 낮게 둔 채 필드를 채우는 것이고, 둘째는 라이브러리가 의도된 선택으로 읽는 0 값을 필드에 둔 채 버전을 올리는 것입니다
- 필드는 설정됐는데 버전이 너무 낮음. version 2 구조체에
m_BrotliEnabled= 1을 쓰면 PDFium은 끝내 그것을 보지 않습니다. 호출은 성공하고 Brotli 스트림은 디코딩 불가로 남습니다. 방어는 하드코딩하는 대신 실제로 쓰이는 필드에서 버전을 유도하는 것이고,LoadLibrary가 하는 바로 그것입니다 - 버전은 충분한데 0 필드가 의미를 가짐. 버전을 6으로 올리면 version 6까지의 모든 필드가 이제 살아 있습니다.
FillChar는m_RendererType을FPDF_RENDERERTYPE_AGG로 0으로 만드는데, 그것은 "설정 없음"이 아니라 진짜 렌더러입니다. 방어는 고른 버전이 커버하는 모든 필드에 의도된 값을 쓰고, "기본값"을 가정하는 대신 실제 빌드에 대해 해석하는 것입니다
호출받는 쪽을 크래시시킬 수 있는 값에는 세 번째 규칙이 따라옵니다. 호출 전에 이진수가 할 수 있는 일에 대해 가장 강한 증거로 검증하고, 그 증거가 휴리스틱일 때는 코드와 문서에서 정직할 것. export된 심볼은 증거입니다. 문자열 테이블의 크레이트 이름은 좋은 추측입니다
빠른 참조: PDFium Component 라이브러리 설정
- 무엇이든 DLL을 로드하기 전에
ConfigurePdfLibrary를 한 번 호출할 것. 어떤 기능 조회든 문서 로드든 봉인합니다 BrotliEnabled나IsolatePerDocument를 설정하고 번들된 런타임에서 Skia 출력을 기대한다면 v3.123.0 이상으로 업그레이드할 것- 특정 래스터라이저가 필요 없다면
Renderer는prpDefault로 둘 것. 이제 모든 구조체 버전에서 빌드 기본값으로 해석됩니다 - 실제로 어떤 렌더러가 활성인지 기록하려면
PdfNativeRendererType을GetSkiaRenderCapabilities.PageRender와 함께 쓸 것 - v3.125.0 이상에서는 AGG 전용 DLL에 대한
prpSkia나 Fontations 없는 DLL에 대한pfbpFontations가 크래시가 아니라EPdfError로 끝날 것을 예상할 것 - 기능 거절 뒤에는
PdfLibraryConfigurationSealed가 False이고 재설정할 수 있습니다. 실패한 DLL 로드 뒤에는 True로 유지됩니다 - Fontations 탐지는 휴리스틱으로 다루고 FreeType 폴백을 갖출 것
- Win32와
TComponent이름 충돌을 피하려면PDFium.LoadLibrary와PDFium.Loaded를 유닛 이름과 함께 쓸 것
설정이 문제되기도 전에 DLL이 실패한다면 Delphi에서 PDFium DLL 로드 실패 진단하기부터 시작하고, 컴포넌트가 각 플랫폼에서 올바른 바이너리를 찾는 방법은 어떤 대상에서든 PDFium 네이티브 라이브러리 로드하기를 참조하세요. 렌더러가 정리되면 렌더 캐시와 부드러운 줌 전략이 뷰어에서 페이지 렌더링을 빠르게 유지하는 방법을 다룹니다
PDFium Component는 Delphi와 C++Builder용 PDFium 엔진을 이런 설정 검사와 함께 감싸므로, 네이티브 초기화는 프로세스 종료가 아니라 여러분이 처리할 수 있는 Pascal 예외로 실패합니다. 제품 세부와 다운로드는 Delphi용 PDFium Component 제품 페이지에 있습니다