Bài viết kỹ thuật

Phát trực tuyến các tệp PDF khổng lồ theo yêu cầu bằng PDFium trong Delphi

Một kho lưu trữ được quét có thể chạy lên tới vài gigabyte trong một tệp PDF duy nhất. Một trình xem mà mở một tệp như vậy thường thì chỉ muốn hiển thị một trang, có thể là trang mục lục, có thể là một trang mà người dùng đã nhảy tới từ một dấu trang (bookmark). Việc đọc toàn bộ tệp vào bộ nhớ để chỉ kết xuất (render) hai trang là một sự lãng phí trên mọi phương diện: nó đốt cháy không gian địa chỉ, nó kìm chân người dùng phía sau một lần đọc khởi tạo mất quá nhiều thời gian, và đối với một quy trình Delphi 32-bit thì nó có thể thất bại hoàn toàn trước cả khi có một trang nào xuất hiện. PDFium được xây dựng với điều này trong tâm trí. Nó có thể tải một tài liệu thông qua một hàm gọi lại (callback) có chức năng yêu cầu các phạm vi byte cụ thể mà nó cần, vào đúng lúc nó cần, và nó sẽ không bao giờ yêu cầu toàn bộ tệp cùng một lúc. Mặc dù vậy, có một ranh giới cần phải nói rõ ngay từ đầu: kênh phát trực tuyến (streaming channel) này mô tả tệp bằng một chiều dài 32-bit, do đó, nó chỉ phục vụ cho một tệp duy nhất có kích thước lên đến 4 GiB, trên thực tế thì như vậy là đã bao phủ gần như toàn bộ mọi kho lưu trữ. Một tệp tin vượt quá ranh giới đó sẽ không nằm trong phạm vi của bài viết này; nó đòi hỏi được chia nhỏ ra thành các khối (volumes) trong lúc quét hoặc được mở thông qua một chiến lược truy cập trực tiếp thay thế, và ở trong Delphi, phần mã bảo vệ để thực thi việc chặn lại ở mức trần này nói một cách thành thực thì sẽ có cả một phần nói riêng ở ngay bên dưới

Bộ component hiển thị đường dẫn đó thông qua một bộ chuyển đổi luồng (stream adapter). Bạn cấp cho nó bất kỳ một TStream nào, và PDFium sẽ kéo các khối (blocks) từ luồng (stream) đó theo đúng như yêu cầu (on demand). Tệp tin có thể được đặt nằm trên đĩa, trong một trường blob cơ sở dữ liệu, hoặc đằng sau bất kỳ một kiểu đối tượng kế thừa nào khác từ TStream, và sẽ chẳng có thứ gì trong số đó được sao chép toàn bộ vào bộ nhớ ngay từ đầu

Cách PDFium yêu cầu các byte

C API của PDFium nạp một tài liệu từ một đối tượng do người gọi (caller) cung cấp được miêu tả bằng cấu trúc FPDF_FILEACCESS. Cấu trúc có ba phần đáng quan tâm ở đây: một trường độ dài, một lệnh gọi lại quá trình đọc, và một tham số người dùng kiểu vô hướng (opaque). Entry point mà tiếp nhận cấu trúc đó là FPDF_LoadCustomDocument. Một khi PDFium nắm giữ cấu trúc đó, nó sẽ phân tích phần đoạn cuối (trailer), xác định vị trí của bảng tham chiếu chéo (cross-reference table), và kể từ đó nó sẽ chỉ đọc những gì mà một thao tác được giao yêu cầu. Việc mở tài liệu sẽ chỉ đụng tới phần đuôi của tệp và một số lượng nhỏ các đối tượng danh mục (catalog objects). Việc kết xuất (rendering) trang 400 sẽ đọc các luồng nội dung và tài nguyên cho trang đó và chẳng đọc thêm cái gì khác

Đây là sự khác biệt giữa tải có đệm (buffered load) và tải phát trực tiếp (streaming load). Một bản tải có đệm sẽ đọc tệp từ đầu đến cuối trước cả khi PDFium nhìn thấy byte số 0. Một bản tải phát trực tiếp sẽ đảo ngược lại mối quan hệ: PDFium mới là bộ phận điều khiển việc đọc, và những byte nào không bao giờ bị đụng tới sẽ không bao giờ được đọc. Đối với một tệp nhiều gigabyte được xem theo từng trang một, đó là khoảng cách giữa một bản tải không thể sử dụng được và một bản tải có thể sử dụng ngay tức thì

Bộ điều hợp luồng (The stream adapter)

Bộ điều hợp đóng vai trò là cầu nối một TStream của Delphi với FPDF_FILEACCESS chính là TPdfStreamAdapter. Hàm khởi tạo (constructor) của nó sẽ lấy luồng và cờ báo quyền sở hữu (ownership flag), bắt lấy thông số độ dài của luồng một lần, điền vào bản ghi FPDF_FILEACCESS, rồi sau đó đấu nối lệnh gọi lại việc đọc. Khi PDFium sau đó gọi lại với một vị trí (offset) và kích thước, bộ điều hợp sẽ dò tới vị trí đó trên luồng rồi sao chép lại chính xác vùng cần lấy vào phần bộ đệm mà PDFium đã cung cấp

// Verbatim from the component: the stream-to-FPDF_FILEACCESS bridge
constructor TPdfStreamAdapter.Create(AStream: TStream; AOwnsStream: Boolean);
begin
  inherited Create;
  if AStream = nil then
    raise EPdfError.Create('TPdfStreamAdapter: AStream is nil');
  FStream := AStream;
  FOwnsStream := AOwnsStream;

  // FPDF_FILEACCESS.m_FileLen is a 32-bit unsigned long. Refuse a stream
  // that would silently truncate past 4 GiB.
  if AStream.Size > High(FPDF_DWORD) then
    raise EPdfError.Create('TPdfStreamAdapter: stream exceeds the 4 GiB limit');

  FillChar(FFileAccess, SizeOf(FFileAccess), 0);
  FFileAccess.m_FileLen  := FPDF_DWORD(AStream.Size);
  FFileAccess.m_GetBlock := GetBlockCallback;
  FFileAccess.m_Param    := Self;
end;

Cờ sở hữu sẽ quyết định xem ai là người giải phóng luồng. Việc truyền False có nghĩa là người gọi sẽ tiếp tục giữ luồng và phải chịu trách nhiệm giữ cho nó hoạt động trong suốt toàn bộ vòng đời của tài liệu. Chuyển nó thành True và bộ điều hợp sẽ tiếp quản điều đó, nó sẽ giải phóng luồng sau khi tài liệu bị đóng. Dù bằng cách nào thì luồng cũng phải tồn tại lâu hơn mọi quá trình đọc mà PDFium sẽ thực hiện, bởi vì PDFium sẽ nắm giữ con trỏ FPDF_FILEACCESS và sẽ gọi lại tại bất kỳ thời điểm nào miễn là lúc đó tài liệu vẫn đang được mở, chứ không phải chỉ trong lần nạp dữ liệu ban đầu

Tại sao gọi lại (callback) lại là một hàm tĩnh (static function)

Hàm gọi lại (callback) dùng để đọc mà PDFium lưu trữ bên trong m_GetBlock là một con trỏ hàm thuần C (plain C function pointer) chứa quy ước gọi (calling convention) cdecl. Một phương thức của Delphi thì không thể được sử dụng trực tiếp, bởi vì một phương thức luôn luôn mang theo một tham số ẩn danh Self mà cái mã gọi lệnh (caller) của C thì hoàn toàn chẳng biết một tí gì về nó và cũng chẳng bao giờ cung cấp. Bởi vì lẽ đó, bộ điều hợp đã khai báo lệnh gọi lại dưới dạng một class function được đánh dấu cdecl; static, cái mà sau khi được biên dịch sẽ trở thành một hàm độc lập mang một bố cục khung (frame layout) của C đúng như những gì mà PDFium kỳ vọng và không còn có mặt của Self ẩn danh

Điều đó đã giải quyết được vấn đề về quy ước gọi nhưng đồng thời cũng làm dấy lên một câu hỏi thứ hai: vì không có Self, bằng cách nào mà hàm gọi lại có thể tiếp cận được chính xác cái luồng cụ thể (specific stream) mà nó được yêu cầu đọc từ đó? Câu trả lời ở đây là tham số người dùng vô hướng. Khi bộ điều hợp tiến hành xây dựng bản ghi (record), nó sẽ cất giữ chính con trỏ chỉ tới đối tượng khởi tạo của nó vào m_Param. PDFium sẽ hoàn trả lại cái con trỏ này vào vị trí đối số (argument) đầu tiên ở mọi lệnh gọi lại. Hàm tĩnh (static function) sẽ ép kiểu (casts) con trỏ này để nó biến đổi trở lại thành một TPdfStreamAdapter, rồi sau đó phái đi yêu cầu lệnh đọc để nhắm thẳng vô cái luồng của chính bản đối tượng thực (instance) đó. Đây là kiểu bệ đỡ tiêu chuẩn (standard trampoline) thường được dùng để giao lại bối cảnh của đối tượng (object context) băng qua một ranh giới C nơi hoàn toàn không có khái niệm về các đối tượng

// Verbatim from the component: the cdecl trampoline back to the instance
class function TPdfStreamAdapter.GetBlockCallback(
  param   : Pointer;
  position: FPDF_DWORD;
  pBuf    : PByte;
  size    : FPDF_DWORD): Integer; cdecl;
var
  Adapter: TPdfStreamAdapter;
begin
  Result := 0;
  if (param = nil) or (pBuf = nil) or (size = 0) then
    Exit;
  Adapter := TPdfStreamAdapter(param);   // recover the instance from m_Param
  if Adapter.FStream = nil then
    Exit;
  try
    Adapter.FStream.Position := Int64(position);
    Adapter.FStream.ReadBuffer(pBuf^, Int64(size));
    Result := 1;
  except
    Result := 0;  // report failure by return value, never by raising
  end;
end;

Mức trần 4 GiB và tại sao nó cần đến một chốt chặn bảo vệ

Đến phần này là ta nói về nguyên do đằng sau cái biên giới (boundary) từng được đề cập tới ở phần mở bài. Trường độ dài m_FileLen trong FPDF_FILEACCESS là một giá trị không dấu (unsigned) loại 32-bit. Độ dài có thể đại diện lớn nhất của nó là 4 GiB trừ đi một byte. Một TStream sẽ luôn báo cáo lại kích thước của nó là Int64, thế nên một cái luồng có thể trình độ mô tả cho vô vàn cái byte so với khoảng mà một trường có thể gánh chứa nổi. Một khi mà cái ngưỡng đó của kích thước một cái luồng lại đạt xa quá mức cái độ trần, thì dĩ nhiên là sẽ chẳng còn chút thực tế nào để có thể đem trình bày trung thực thông tin xem độ dài của tệp tin đến PDFium nữa

Hành vi phản xạ sai lầm nhất chính là tiến hành gán luôn kích thước, và mặc kệ nó đem bọc (wrap). Việc thực hiện gọt bỏ bớt phần độ dài của 5 GiB về còn xuống vừa trong một cái chỗ đứng (field) kích thước 32-bit để lại sẽ thành là thứ con số bé tẹo trông bộ cũng không đến nỗi nào lắm, và khi đó PDFium sẽ đi làm trò phân tách tệp vì tin tưởng chắc rằng cái kết thúc của nó mới đâu lân la cỡ độ một cái gigabyte vào. Phần trailer và phần bảng định vị cho điểm đến theo kiểu mảng dữ liệu qua lại (cross-reference table) tồn tại yên ổn ngay sát đoạn khúc đuôi tệp, lố phần cái đường dài đó ra, đâm thế ra cái quá trình tra cú pháp tự lủng (fails) mang một sắc vẻ biểu hiện (way) không dính líu chi đến nguyên do thật. Lỗi mà bạn sẽ phải đang gỡ là cái chuyện báo sai về định vị tham chiếu ngang với bản file tin chẳng có rắc rối thực thà chút nào sất, mà chả ai chỉ ra để hay rằng cái chuyện là nguyên cả một dãy số con (integer wrapped) lại mang bị cuộn lòi ngược trên thêm vài ba lớp (layers up)

Bộ điều hợp (adapter) do đó bèn ngay lập tức khước từ hẳn đường dẫn (input). Khối lệnh sinh ban đầu so tính luôn cái kích thước độ phân khối luồng cùng cho thằng High(FPDF_DWORD) và phát sinh một sự thể từ EPdfError luôn cái độ chớp mắt tại giây cái đường luồng bị bự hơn so bề sức chứa khả năng cho phép của nó. Cứ chớp tung rõ ngay một chuyện rành ràng báo ngay phần sự thể bị thực tình ở thời điểm nhào đắp nên khung (point of construction). Tự chặt gọn thu bé ở bề ngầm tàng khuất đi cái đống gốc chóp sau sau đó bằng triệu trạng mập mờ mà các cậu thợ làm sẽ cứ hục vào rượt theo mãi mút đằng dài về sau. 4 GiB chặn đường thẳng tưng rập là độ bị bức bách bó (constraint) quá rành ràng của riêng đường mạch theo chuyện lấy phần nhập lối này, và những phần xử tốt nhất là phải lộ đưa cả bộ cái thứ này cho chưng to ầm vang thay bằng khỏa lấp đem trải trùm toán học có cơ đi ngả nào thế nó hên mà kết đúc dính rập lấy (compile). Đến thời đụng vào cái cục tệp đã lố khỏi cái viền ranh này (crosses the line), thì biện pháp y như đà được nêu hồi ban nãy nay lòi hiện bóng ra hẳn bên ngoài chỗ ranh cấp API này ngay: xé phần máy quét ra cái đường đống những phôi (per-volume files) và bắt làm sao rập lại vẫn dưới khoảng ngưỡng trên, không thế đành phải bỏ tài liệu kia đóng yên trơ ngay đó trên đĩa ngạnh và cung phụng theo hướng đồ mưu bằng mở theo đường rẽ kiểu mảng trực diện (direct-access design) lên cỗ các gốc số điểm ngắm nhích của 64-bit phó thác lại bằng cho chuyện không dựa trên con đèo tên FPDF_FILEACCESS

Lỗi hỏng không được phép vượt qua biên giới (cross the boundary)

Một quá trình đọc lại hoàn toàn có rủi ro trục trặc. Đường mạch luồng rẽ đấy rất có thể là rập khuất hệ mạng lưới (network-backed) thành ra bị vướng lố thời giờ ngưng (times out), một vật phôi dạng blob đã đứt chỗ kết dính ngay dưới tay mình, hay bẹp dí (truncated) lúc đang chường mặt vào. Bản thỏa ước hợp (contract) phía với PDFium giao để đón nhịp nối lệnh vòng quay cho nhịp chuyện nhặt đọc là theo giá tính báo lại hồi nại mang đáp kết (return value): thành công là phi không (non-zero), hỏng bét là nhịp cái báo tại một chỗ 0 tròn. Ở bề là đường C frame (rập ván chuẩn C), và ở chẳng chỗ nào lại chường ngầm mang đệm lưới giăng đỡ trượt nhịp hay đà mang đùn lên truyền vẩy đợt của lỗi Pascal (Pascal exception)

Ấy chính là sao mà bệ nảy kia bao khối cái việc mưu chuyện truy tìm đường cùng đà nhặt lật (seek and the read) bên ngay chóp của phần bọc try/except cái mà sẽ đi ngoạm cả nấc hụt rập trượt của đường ngoại rẽ hòng đáp hồi tại nấc 0. Dù có để mà bệ nhún ngoại vi bị vỡ bên Pascal phóng lên trào lòi cả ra vũng cái ranh nẩy trả đà gọi kia, việc xả cuộn kia sẽ bóc nát tan tành lớp vỏ đệm tại các lớp lề rập gọi cdecl xếp hàng theo của nơi PDFium's (PDFium's cdecl stack frames), mấy mảng rập lớp từ thuở nguyên sơ vốn đâu đúc bện để hòng hớn hở chào cái trớ trêu xả rách xé bóc tự chỗ mảng bộ vung đẩy bắt chặn của bên mảng khối chuyện Pascal (Pascal exception machinery). Hậu sự đứt quãng là chuyện bãi đàng bét bè nhè thì may (undefined behavior) mà bét hỏng tung hoang ra cái bãi nhão cứng hỏng rập cỗ là đen sâu dưới rập tầng dưới đáy bộ cỗ PDF, bộ gọi đà chạy sập hỏng và bộ dõi (stack) bét hết vô dụng rập. Phục hồi và gửi số 0 (Returning zero) lại bảo trợ nhốt được hụt hỏng ở kín tròng bộ hẹn. PDFium thâu ngắm thấy mớ luồng khối hỏng đường lướt đọc lại, cắn đứt rập khối hoạt trình gãy đoạn tinh sạch nhẹ (aborts the operation cleanly), và ở đó là cỗ FPDF_LoadCustomDocument ré lên một chặp cho báo tài liệu thì hỏng cả trượt việc đón vô, đó mới thành cỗ thành cụm phát cái lên trên lộ diện tại phần bục (surfaces) biến báo danh của EPdfError về bên mé chốn gốc Pascal, mảng ở chốn của nó thuộc địa (where it belongs)

Mở một tài liệu theo cách này

Hàm từ component có nhiệm vụ vận hành cái hệ thống xử lý phát trực tiếp này là hàm LoadCustomDocument, được thông báo đứng ở vị trí hẳn là phần chức năng khác (distinct method) thay cho chuyện nó chỉ như là một lần quá nạp (overload) khác của hàm LoadDocument, giúp đỡ tránh được điều khi vô ý bỏ nhầm cái luồng nhúng theo gốc mảng nạp nhớ (TMemoryStream) nhầm chệch đáp lên cái nhánh bộ đệm (buffered path). Nó xây nên bộ cài chuyển đổi (adapter), khởi xướng phần FPDF_LoadCustomDocument, và đảm trách ôm sát chuyện bộ máy cài cho vẫn theo nhịp bám giữ với nguyên vẹn luồng của thời tài liệu đã nạp lấy (loaded document)

var
  Pdf: TPdf;
  FileStream: TFileStream;
begin
  Pdf := TPdf.Create(nil);
  FileStream := TFileStream.Create('Archive_4GB.pdf', fmOpenRead or fmShareDenyWrite);
  try
    // Hand stream ownership to Pdf: it frees FileStream when the document closes.
    Pdf.LoadCustomDocument(FileStream, True);
    // PDFium has read only the trailer and catalog so far.
    // Rendering a page pulls just that page's bytes through the callback.
    // ... render or inspect pages here ...
  finally
    Pdf.Free;  // closes the document, which frees the adapter and the stream
  end;
end;

Cách gọi lệnh cũng xài được hệt đối của TMemoryStream, hay loại chuỗi nhánh luồng gốc dạng nhầy như blob stream ở dữ bộ từ dataset (database dataset), hoặc tùy biến của dòng giòng nhánh (descendant) từ bên TStream. Đường kéo lấy trích nạp ở trạng rẽ của theo gọi (On-demand loading) kiếm lại được công gánh đáng vả lúc tệp tập tin cực nặng (large) và chỉ bưng một gốc riêng biệt sẽ dính vô cái sự để trích (read): công cụ nhìn dòm mớ hệ dồn lưu trữ (archive viewer), bộ cỗ đúc ra ảnh miểng cho liếc ngang thu cỡ mẩu (thumbnail generator) đi rớt dính có mấy tờ một, bộ đánh chỉ tra (search index) hất giật tờ đi mỗi cú một lượt (pulls one page at a time). Nếu mà file kia bé hin (small) hay hễ gì bạn cũng đi móc đọc láng cả đống sạch đi trơn (read all of it anyway), lướt cho tải ôm theo lề đệm nạp (buffered load) thì đơn sơ hơn và mớ cỗ xủng xẻng đường rẽ gọi lúc đòi đơm luồng thì (streaming machinery) chả mưu việc được chi hết cho cậu. Mối rẽ chốt ở nơi (deciding factor) đó là cái cỡ cân (ratio) về hệ mức độ các bytes ở mặt thực thà bấu vô sờ vô trên những tổng toàn các dung mớ lượng ôm sẵn chứa bên trong cả cái tệp kia

Hễ đà mà mớ trang tờ kéo xối trào dòng đổ về ngay của những phút đòi đòi bứt lệnh, mối ngóng đợi bận vào đằng tiếp theo (next concern) là làm cho bám nhịp (responsive) cùng bộ đống những trang rập đà trồi vẽ xì kết ra nhịp cho tay người rờ mó thọc (zooms and scrolls), thứ bao trùm ở trong chỗ chỗ note bãi này của bọn chúng mình bàn cái cache khâu kết xuất dính dáng trượt hiệu lực nạp cái phóng to bự (render caching and zoom performance). Giả dụ gốc của tờ chuyển luồng nằm trọn gói mà cái người nhòm tài liệu chỉ hòng hóng qua lại chặn tuốt luột chẳng thả cái kẻ đọc phọt đùn xì đùn mớ đi mẻ bớt hay đâm dính làm rập sai (export or alter), khối cách kỹ tháo vát nơi cỗ chạy ngầm bọc đường kín dòm rà (secure PDF preview walkthrough) đâm sáp vô khít hụi khéo điệp ở cái mánh đèo luồng kéo đọc lúc bấy giờ. Bộ cả hai cùng xây cất nên (build on) nền của kiểu luồng tải tuôn trực (streaming load) rập trọn cho bài bàn tán này, được mang tống đi như khối cục bộ ruột tùng trong PDFium Component của chốn Delphi cũng kèm vô với nhóm bên C++Builder nằm chen với chốn đà bám rịt kết xuất (rendering), nhổ kéo văn tự (text extraction), cùng API chuyên đi phụ đánh dấu chữ móc câu ghi đè (annotation APIs) được vây trùm bàn bét ở chốn các ngã dạt quanh tại dải chỗ blog xóm xó đây