Bài viết kỹ thuật

PDFium Library Config: Khi nào Brotli lặng lẽ đổi Skia

Trong PDFium Component cho Delphi, việc bật BrotliEnabled hay IsolatePerDocument trong TPdfLibraryConfiguration từng lặng lẽ đổi bản dựng Skia đóng gói sang renderer AGG mà chẳng báo lỗi gì, vì cả hai tùy chọn nâng FPDF_LIBRARY_CONFIG lên một version mà PDFium đọc m_RendererType theo nghĩa đen. Kể từ v3.123.0, renderer mặc định giữ nguyên mặc định riêng của DLL, và kể từ v3.125.0, một lời đòi Skia hay Fontations mà DLL không đáp ứng được nêu một EPdfError có thể bắt thay vì giết tiến trình

Chẳng bug nào tự giới thiệu. Cái đầu tạo ra các trang nhìn có vẻ ổn, chỉ là render bởi một rasterizer khác, với anti-aliasing và cạnh chữ hơi khác bản bạn đã ship và test. Cái hai thì tự giới thiệu, ầm ĩ, bằng cách kéo tiến trình chủ sụp từ bên trong phần khởi tạo native. Cả hai đều đến từ cùng một chỗ: một cấu trúc C có version mà các field chỉ có nghĩa khi con số version nói thế, và các giá trị không của nó không phải "chưa đặt" mà là những lựa chọn thật

FPDF_LIBRARY_CONFIG quyết định PDFium dùng renderer nào thế nào?

FPDF_InitLibraryWithConfig chỉ thăm m_RendererType khi field Version của cấu trúc từ 4 trở lên, và từ version đó trở đi nó dùng giá trị đúng như viết. Dưới version 4, PDFium bỏ qua field và chọn mặc định của bản dựng, là Skia ở các bản dựng compile với PDF_USE_SKIA và AGG ở mọi nơi khác

Mỗi field sau đó đi theo cùng một mẫu. Cấu trúc lớn thêm một capability mỗi lần, và mỗi capability đến cùng một số version mới. PDFium Component dựng cấu trúc native trong LoadLibrary từ TPdfLibraryConfiguration của bạn và chỉ nâng version đúng mức mà các tùy chọn bạn đặt đòi hỏi

Version cấu trúcField nó thêmĐặt bởi
2m_pIsolate, m_v8EmbedderSlotLuôn được ghi; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform khác nil
4m_RendererTypeRenderer khác prpDefault
5m_FontLibraryTypeFontBackend khác pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = True

Cái bẫy nằm ở hai hàng cuối. Version là cộng dồn: một cấu trúc version 6 cũng là một cấu trúc version 4 và version 5, nên PDFium đọc m_RendererType và m_FontLibraryType dù bạn chỉ đòi Brotli. Bất cứ thứ gì nằm trong hai field đó tại khoảnh khắc ấy trở thành renderer và font backend, dù bạn có định chọn chúng hay không

Bậc thang version FPDF_LIBRARY_CONFIG của PDFium Component từ version 2 tới version 7 cho thấy tùy chọn TPdfLibraryConfiguration nào thêm m_RendererType, m_FontLibraryType, m_BrotliEnabled và m_IsolatePerDocument, và vì sao version cộng dồn biến một field renderer bằng 0 thành một lựa chọn AGG có chủ đích thay vì một giá trị chưa đặt trên mọi bản dựng
Mỗi tùy chọn nâng version cấu trúc và mọi field trước đó vẫn sống, nên con số 0 trong m_RendererType đến với PDFium như một lời đòi AGG tường minh

Vì sao bật Brotli lại đổi renderer sang AGG?

Trước v3.123.0, PDFium Component ghi FPDF_RENDERERTYPE_AGG vào m_RendererType cho prpDefault, nên bất kỳ cấu hình nào đẩy cấu trúc lên version 6 hay 7 đều ép AGG lên một bản dựng Skia. Các runtime pdfium.dll và pdfium.v8.dll đi kèm component là các bản dựng Skia, nên cái này trúng deployment mặc định, chứ không phải một cấu hình lạ

Phép ánh xạ nhìn vô hại khi viết ra. Ở version 2 hay 3, field không bao giờ được đọc, nên prpDefault thật sự nghĩa là "DLL làm gì thì theo". Khoảnh khắc BrotliEnabled (version 6) hay IsolatePerDocument (version 7) bước vào cuộc, cùng đoạn code đó biến "không có ý kiến" thành một lời đòi AGG tường minh. Chẳng gì hỏng. PDFium khởi tạo bình thường, render mọi trang, và trả về không có error code nào, vì dưới góc nhìn của nó, caller đã đòi AGG và nhận AGG

Một pixel hash làm nổi cuộc hoán đổi ở nơi screenshot không thấy. Render trang đầu của cùng một tài liệu mẫu dưới ba cấu hình cho:

  • Cấu hình mặc định: hash 502D77C3711B4ACF
  • BrotliEnabled = True với Renderer để nguyên prpDefault: hash F75B5EB4728ADE87
  • prpAgg tường minh: hash F75B5EB4728ADE87, y hệt lần chạy Brotli

Bản sửa trong v3.123.0 là hàm công khai PdfNativeRendererType, hàm phân giải một TPdfRendererPreference thành giá trị ghi vào m_RendererType. prpAgg và prpSkia ánh xạ một-một. prpDefault giờ phân giải thành Skia khi DLL đã nạp export FPDF_RenderPageSkia và thành AGG trong trường hợp ngược lại. Export đó được compile dưới cùng điều kiện PDF_USE_SKIA với chính mặc định Skia, điều khiến nó trở thành thuộc tính bản dựng duy nhất bạn quan sát được từ ngoài DLL. Sau bản sửa, cấu hình Brotli cho cùng hash với cấu hình mặc định

So sánh pixel hash của PDFium Component cho thấy hash render Skia mặc định 502D77C3711B4ACF, cấu hình BrotliEnabled trước v3.123.0 trùng với lần chạy prpAgg tường minh với hash F75B5EB4728ADE87, và wrapper đã sửa phân giải prpDefault qua export FPDF_RenderPageSkia về hash Skia ban đầu
Pixel hash bắt được thứ screenshot giấu: bật Brotli từng render mọi trang bằng AGG, còn mặc định đã sửa giờ khớp cấu hình nguyên vẹn

Font backend chưa từng dính cùng vấn đề. m_FontLibraryType được đọc từ version 5, và giá trị không của nó, FPDF_FONTBACKENDTYPE_FREETYPE, cũng là mặc định của PDFium khi field không được đọc chút nào. Ghi FreeType cho pfbpDefault vì thế tái hiện đúng mặc định native. Giá trị không không phải lúc nào cũng sai, chỉ là không bao giờ tự động đúng

Với v3.123.0 trở lên, đoạn code startup bạn tự nhiên sẽ viết giờ làm đúng như nó nói:

uses
  PDFium;

procedure ConfigurePdfiumAtStartup;
var
  Config: TPdfLibraryConfiguration;
begin
  // Phải chạy trước khi bất cứ thứ gì nạp thư viện native
  Config := TPdfLibraryConfiguration.Default;
  Config.BrotliEnabled := True;   // nâng FPDF_LIBRARY_CONFIG lên version 6
  // Renderer giữ prpDefault: phân giải thành Skia trên các bản dựng export
  // FPDF_RenderPageSkia và thành AGG trên các bản dựng chỉ AGG
  SetLength(Config.UserFontPaths, 1);
  Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
  ConfigurePdfLibrary(Config);
end;

Nhớ rằng BrotliEnabled chỉ khiến các stream PDF 2.0 /BrotliDecode giải mã nổi khi chính DLL được build với PDF_ENABLE_BROTLI. Cờ là một lời đòi, và trên một bản dựng không hỗ trợ Brotli nó chẳng có tác dụng gì. TPdfLibraryConfiguration.Hardened giống Default trừ chỗ AllowMachineTime là False, chặn JavaScript tài liệu đọc đồng hồ thật; đó là một điểm khởi đầu hợp lý cho việc xử lý phía server các tệp không đáng tin

Chuyện gì xảy ra khi bạn đòi một backend mà DLL không chứa?

PDFium không trả về lỗi cho một renderer hay font backend vắng mặt trong bản dựng: FPDF_InitLibraryWithConfig làm rơi một CHECK native, thứ trên Windows lộ ra như một breakpoint exception và, thiếu một structured exception handler bao quanh lời gọi, chấm dứt tiến trình. Header nói thẳng điều đó, cảnh báo một giá trị không được hỗ trợ "will similarly fail with an immediate crash"

Hai trường hợp cụ thể là một bản dựng chỉ AGG nhận FPDF_RENDERERTYPE_SKIA, và một bản dựng thiếu Fontations nhận FPDF_FONTBACKENDTYPE_FONTATIONS. Runtime Skia đóng gói nằm ở nhóm thứ hai: nó render bằng Skia nhưng dùng FreeType cho font. Đòi prpSkia cùng pfbpFontations với nó đã cho ra External exception 80000003 phía Delphi. Khi debugger hay một exception handler tình cờ bắt được nó, tình hình vẫn không cứu nổi:

  • PDFium bị bỏ lại nửa khởi tạo
  • Cấu hình toàn tiến trình đã niêm phong, nên ConfigurePdfLibrary từ chối một cấu hình đã sửa
  • Thử lại với một cấu hình khác trong cùng tiến trình không còn khả thi

Đây là thất bại ngược chiều với bug Brotli. Ở đó, field giữ một giá trị chẳng ai chọn và PDFium lặng lẽ chấp nhận. Ở đây, field giữ một giá trị caller chọn có chủ đích và PDFium không nhận bất kỳ cuộc thảo luận nào về nó. Cả hai đều là vấn đề mà một wrapper phải giải quyết trước lời gọi native, vì sau nó chẳng còn gì để bắt

PDFium Component precheck Skia và Fontations thế nào

Kể từ v3.125.0, LoadLibrary kiểm chứng cấu hình sau khi buộc các export DLL và trước khi gọi FPDF_InitLibraryWithConfig, và biến một renderer hay font backend không được hỗ trợ thành một EPdfError với thông điệp nêu tên thiết lập vi phạm cùng các phương án thay thế. DLL được dỡ xuống và cấu hình được mở niêm phong, nên caller có thể chọn thiết lập khác rồi nạp lại

Phán quyết nằm trong hàm thuần túy PdfLibraryConfigurationSupportError, hàm nhận cấu hình cộng hai Boolean mô tả bản dựng và trả về một chuỗi rỗng khi tổ hợp là an toàn. Vì nó không đụng trạng thái native nào, bạn có thể gọi nó từ test của riêng mình với bất kỳ tổ hợp capability nào. Bên trong LoadLibrary, hai Boolean đó đến từ những loại bằng chứng khác nhau, và chúng đáng mức tin khác nhau:

  • Skia được phát hiện từ sự hiện diện của export FPDF_RenderPageSkia, đúng tín hiệu mà PdfNativeRendererType dùng. Export và renderer Skia được compile dưới một điều kiện, nên phép kiểm tra là chính xác
  • Fontations không có export riêng. Dấu vết duy nhất nó để lại là các font crate Rust nó kéo vào binary, nên PDFium Component quét tệp thư viện đã nạp tìm các tên crate skrifa và read-fonts (cả read_fonts). Phép quét chỉ chạy khi pfbpFontations được đòi, và một tệp không đọc nổi được tính là "không có Fontations"

Phép kiểm Fontations là một heuristic, và nó có thể sai theo một chiều: một bản dựng Fontations bị strip mất tất cả các chuỗi đó sẽ bị từ chối dù nó lẽ ra chạy được. Cuộc đánh đổi này được đưa ra có chủ đích. Một lần từ chối nhầm lấy đi của bạn một exception có thể bắt và một fallback về FreeType. Một lần chấp nhận nhầm lấy đi của bạn cả tiến trình

Mở niêm phong quan trọng ngang phép kiểm tra. LoadLibrary niêm phong cấu hình ngay đầu lúc nạp, nên không có phần reset, một lần từ chối capability sẽ để ConfigurePdfLibrary đáp mọi lần thử lại bằng EPdfError "PDFium library configuration is already sealed". Đường từ chối gọi UnloadLibrary trước; lời gọi FPDF_DestroyLibrary của nó an toàn tại điểm đó vì PDFium chưa được khởi tạo và trả về ngay lập tức. Các thất bại nạp khác, như thiếu DLL hay lệch kiến trúc, giữ nguyên niêm phong, nên một vòng thử lại phải phân biệt được hai thứ:

uses
  SysUtils, PDFium;

function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
  Config: TPdfLibraryConfiguration;
begin
  Config := TPdfLibraryConfiguration.Default;
  Config.Renderer := prpSkia;
  ConfigurePdfLibrary(Config);
  try
    PDFium.LoadLibrary;   // ghi rõ unit: Windows.LoadLibrary trùng tên
    Result := prpSkia;
  except
    on E: EPdfError do
    begin
      // Một lần từ chối capability dỡ DLL và mở lại niêm phong cấu hình.
      // Một DLL chưa nạp được chút nào vẫn niêm phong: thử lại chẳng cứu được
      if PdfLibraryConfigurationSealed then
        raise;
      Config.Renderer := prpAgg;
      ConfigurePdfLibrary(Config);
      PDFium.LoadLibrary;
      Result := prpAgg;
    end;
  end;
end;

Chú ý cái PDFium.LoadLibrary tường minh. Trong một unit cũng dùng Windows hay Winapi.Windows, một LoadLibrary không ghi rõ unit sẽ phân giải về unit nào xuất hiện sau cùng trong uses clause; khi đó là hàm Win32, lời gọi không tham số không compile nổi với một lỗi đếm đối số chẳng nói gì về PDFium

Luồng precheck LoadLibrary của PDFium Component nơi ConfigurePdfLibrary niêm phong cấu hình, phép kiểm capability thử export FPDF_RenderPageSkia và bằng chứng chuỗi skrifa, một lời đòi không được hỗ trợ nêu một EPdfError có thể bắt và mở niêm phong để thử lại, trong khi một DLL chưa từng nạp được giữ PdfLibraryConfigurationSealed true
Phần kiểm chứng chạy sau khi các export buộc xong và trước khởi tạo, nên một backend vắng mặt hỏng thành một EPdfError bạn bắt được thay vì một CHECK native giết tiến trình

Phần kiểm chứng diễn ra còn sớm hơn

ConfigurePdfLibrary từ chối vài tổ hợp trước khi bất kỳ DLL nào tham gia, tất cả bằng EPdfError. Một FontBackend tường minh, kể cả pfbpFreeType, đòi Renderer = prpSkia, vì PDFium chỉ thăm font backend cho renderer Skia. IsolatePerDocument đòi V8Isolate phải nil, vì PDFium tự tạo isolate cho từng tài liệu và làm rơi một CHECK native nếu bạn cũng trao nó một cái. Chuỗi rỗng trong UserFontPaths bị từ chối. Và bất kỳ lời gọi nào sau lần thử nạp đầu đều hỏng với "PDFium library configuration is already sealed"

Luật cuối có một hệ quả thực dụng: bạn không thể thăm dò DLL trước rồi cấu hình sau. GetSkiaRenderCapabilities, V8FeaturesAvailable, mở một tài liệu, và phần lớn các điểm vào khác đều gọi LoadLibrary nội bộ, thứ niêm phong cấu hình tại chỗ. Gọi UnloadLibrary sau đó cũng không mở lại được. Cấu hình trước, rồi nạp, rồi hỏi, đúng thứ tự mà một routine chẩn đoán nên đi:

uses
  SysUtils, PDFium, FPdfView;

function DescribePdfiumState: string;
var
  Config: TPdfLibraryConfiguration;
  Renderer: string;
begin
  Config := GetPdfLibraryConfiguration;   // một bản sao, soi an toàn
  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;
  // Cùng phép phân giải mà LoadLibrary đã áp khi dựng FPDF_LIBRARY_CONFIG
  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;

Log một dòng đó một lần lúc startup thì rẻ, và nó là thứ đầu tiên bạn muốn trong một support ticket nói rằng "text trông khác trên server". PDFium.Loaded được ghi rõ unit vì cùng lý do với LoadLibrary: bên trong một method form hay component, một Loaded trơn buộc vào TComponent.Loaded

Hai kiểu một cấu trúc C có version đi sai

Mọi cấu trúc cấu hình có version, dù là FPDF_LIBRARY_CONFIG, một record cbSize Win32, hay một plugin ABI, đều hỏng theo hai cách đối xứng, và một wrapper phải phòng cả hai. Thứ nhất là điền một field trong khi để version quá thấp; thứ hai là nâng version trong khi để một field ở giá trị không mà thư viện đọc thành một lựa chọn có chủ đích

  1. Field đặt, version quá thấp. Ghi m_BrotliEnabled = 1 vào một cấu trúc version 2 và PDFium chẳng bao giờ liếc nó. Lời gọi thành công và các stream Brotli vẫn không giải mã nổi. Tuyến phòng thủ là suy ra version từ các field thực sự đang dùng, đúng điều LoadLibrary làm, thay vì hard-code một con số
  2. Version đủ cao, field không mang nghĩa. Nâng version lên 6 và mọi field tới version 6 giờ sống. FillChar đưa m_RendererType về FPDF_RENDERERTYPE_AGG, một renderer thật, không phải "chưa đặt". Tuyến phòng thủ là ghi mọi field mà version đã chọn phủ bằng một giá trị có ý định, và phân giải "mặc định" theo bản dựng thật thay vì giả định

Một luật thứ ba theo sau cho các giá trị có thể làm sập bên được gọi: kiểm chứng chúng với những gì binary làm nổi trước lời gọi, bằng bằng chứng mạnh nhất có sẵn, và thành thật trong code lẫn tài liệu khi bằng chứng đó là một heuristic. Một ký hiệu được export là bằng chứng. Một tên crate trong string table là một phỏng đoán tốt

Tra nhanh: cấu hình thư viện PDFium Component

  • Gọi ConfigurePdfLibrary một lần, trước khi bất cứ thứ gì nạp DLL; bất kỳ truy vấn capability hay lần nạp tài liệu nào cũng niêm phong nó
  • Nâng lên v3.123.0 trở lên nếu bạn đặt BrotliEnabled hay IsolatePerDocument và mong output Skia từ các runtime đóng gói
  • Để Renderer ở prpDefault trừ khi bạn cần một rasterizer cụ thể; nó giờ phân giải thành mặc định bản dựng ở mọi version cấu trúc
  • Dùng PdfNativeRendererType với GetSkiaRenderCapabilities.PageRender để log renderer nào thật sự đang chạy
  • Chờ EPdfError, không phải crash, cho prpSkia trên một DLL chỉ AGG hay pfbpFontations trên một DLL phi-Fontations ở v3.125.0 trở lên
  • Sau một lần từ chối capability, PdfLibraryConfigurationSealed là False và bạn được phép cấu hình lại; sau một lần nạp DLL thất bại nó giữ True
  • Coi việc phát hiện Fontations là heuristic và giữ một fallback FreeType
  • Viết PDFium.LoadLibrary và PDFium.Loaded kèm tên unit để tránh trùng tên với Win32 và TComponent

Nếu DLL hỏng trước khi cấu hình kịp có ý nghĩa gì, hãy bắt đầu với chẩn đoán thất bại nạp PDFium DLL trong Delphi, còn component tìm binary đúng trên mỗi platform ra sao xem nạp thư viện native PDFium trên mọi mục tiêu. Khi renderer đã ổn, chiến thuật render cache và zoom mượt nói về cách giữ việc render trang nhanh trong một viewer

PDFium Component bọc engine PDFium cho Delphi và C++Builder với những phép kiểm cấu hình như thế này, nên phần khởi tạo native hỏng thành một exception Pascal bạn xử lý được chứ không phải một lần thoát tiến trình. Chi tiết sản phẩm và bản tải về nằm trên trang sản phẩm PDFium Component cho Delphi