Bài viết kỹ thuật

Vì sao một số object stream PDF giải mã ra rác trong Delphi

Một object stream PDF giải nén không lỗi nhưng vẫn đọc như nhiễu thường thiếu một bước: đảo ngược ISO 32000-1 Predictor. Khi từ điển /DecodeParms của một stream mang /Predictor 2 trở lên, các byte mà FlateDecode trả về không phải dữ liệu gốc — chúng là các giá trị đã lấy hiệu theo hàng kiểu PNG hoặc lấy hiệu ngang kiểu TIFF cần một lượt tái thiết thứ hai trước khi bất kỳ lượt tra cứu từ điển nào có ý nghĩa. PDFiumPas, thư viện component PDF VCL gốc cho Delphi và C++Builder, đã thêm lượt tái thiết đó trong v2.16.0, cụ thể vì các object stream PDF 1.5+ đang mở rộng thành các byte đã lấy hiệu mà không trình phân tích từ điển nào có thể đọc được

Vì sao chỉ FlateDecode là không đủ

Bản thân FlateDecode chỉ là giải nén DEFLATE (ISO 32000-1 §7.4.4.1): nó tái tạo bất cứ byte nào bộ mã hóa đã đưa cho bộ nén, không hơn. Predictor sống ở một lớp cao hơn, trong từ điển /DecodeParms của stream, và nó mô tả một phép biến đổi mà bộ mã hóa đã áp dụng trước khi nén — việc lấy hiệu biến các chuỗi dài giá trị có cấu trúc tương tự nhau, như các số nguyên đóng gói chặt bên trong một luồng cross-reference hay một object stream, thành các chuỗi dài số nhỏ mà DEFLATE nén tốt hơn nhiều. ISO 32000-1 §7.4.4.3 (Bảng 8) nói rõ ràng rằng việc hoàn tác phép biến đổi này là một phần của việc giải mã một stream đã lọc, không phải một lượt dọn dẹp tùy chọn, thế nhưng dễ viết một helper FlateDecode chỉ gọi inflate rồi dừng lại

Triệu chứng khá đặc trưng một khi bạn biết cần tìm gì. Các byte đã lấy hiệu bởi Predictor không phải nhiễu ngẫu nhiên — chúng vẫn mang hình dạng của một stream đã nén, nên một trình phân tích ngây thơ thường đi qua một vài token trông hợp lệ trước khi đụng phải một chuỗi byte không thể là một tên, số, hay dấu phân cách PDF, và các hàng khác nhau thất bại ở các offset khác nhau tùy theo mức độ các giá trị bên dưới tình cờ khác biệt với các giá trị lân cận. Sự thiếu nhất quán đó là điều khiến lỗi khó xác định từ một file lỗi đơn lẻ: hai PDF từ cùng một nhà sản xuất có thể chỉ khác nhau ở việc giá trị nào tình cờ lặp lại, nên một cái phân tích được gần như tình cờ trong khi cái kia thất bại hoàn toàn

Tham số Predictor của PDF thực sự làm gì?

Entry /Predictor trong /DecodeParms nói cho một trình đọc tuân thủ chuẩn biết cần chạy lượt đảo ngược nào, và ISO 32000-1 Bảng 8 định nghĩa các giá trị quan trọng trong thực tế: 1 nghĩa là không phép dự đoán nào được áp dụng, 2 chọn TIFF Predictor 2 (lấy hiệu ngang), và bất kỳ giá trị nào từ 10 đến 15 chọn phép dự đoán kiểu PNG. Ba khóa nữa đi kèm nó — /Colors, /BitsPerComponent, và /Columns — và cùng nhau chúng mô tả hình học hàng mà việc lấy hiệu đã được tính theo đó, ngay cả khi stream không chứa dữ liệu ảnh nào cả: một object stream không phải một bức ảnh, nhưng các bộ ghi PDF tái sử dụng cùng cỗ máy predictor dựa-trên-hàng cho nó vì delta-rồi-deflate nén các số nguyên và offset đối tượng đóng gói chặt tốt hơn deflate chúng ở dạng thô

TIFF Predictor 2 là lược đồ đơn giản hơn trong hai lược đồ: mỗi thành phần được lưu như hiệu số từ cùng thành phần đó trong pixel trước trên cùng một hàng, và mỗi hàng đặt lại tại cạnh trái của nó thay vì mang một hiệu số vào từ hàng phía trên. Dự đoán PNG cụ thể hơn, vì bộ lọc thực tế có thể thay đổi từ hàng này sang hàng khác: mỗi hàng bắt đầu bằng một byte tag đơn lẻ — 0 cho None, 1 cho Sub, 2 cho Up, 3 cho Average, 4 cho Paeth — và tag đó, không phải giá trị /Predictor đã khai báo, quyết định hàng cụ thể đó được tái thiết như thế nào. Một /Predictor là 12 thực ra chỉ là gợi ý của bộ mã hóa rằng nó ưu ái bộ lọc Up, nơi mỗi byte được khôi phục bằng cách cộng byte trực tiếp phía trên nó trong hàng trước, nhưng một bộ giải mã đúng đắn vẫn phải đọc tag trên mỗi hàng thay vì giả định Up xuyên suốt

Vì sao object stream khiến một Predictor bị bỏ sót trở nên vô hình?

Object stream làm phức tạp vấn đề thay vì chỉ lặp lại nó. ISO 32000-1 §7.5.7 cho phép một bộ ghi PDF 1.5+ đóng gói nhiều đối tượng gián tiếp vào một container nén duy nhất, một /ObjStm, và phổ biến là chính các đối tượng mà một bộ xác thực cần nhất — catalog, /OutputIntents, hay một stream /Metadata XMP — đi qua container đó với /Predictor 12 đính kèm, vì các đối tượng đó đủ ngắn và lặp lại để hưởng lợi từ việc lấy hiệu theo hàng. Khi bước predictor bị thiếu, việc mở rộng object stream không ném ra lỗi: nó tạo ra một chuỗi byte trông có vẻ hợp lý bề ngoài nhưng không token hóa thành các đối tượng mong đợi, nên bất cứ thứ gì được đóng gói bên trong đơn giản là không xuất hiện. Việc render hiếm khi nhận thấy, vì một engine render tuân thủ chuẩn đã tái thiết dữ liệu đã lấy hiệu bởi predictor trước khi nó từng đến được bố cục; code nhận thấy chính xác là loại code mà lỗi này ẩn mình bên trong — một bộ xác thực, bộ ký, hay bộ kiểm tra phiên bản tự duyệt các byte PDF thô để trả lời một câu hỏi cấu trúc, không có phương án dự phòng một khi góc nhìn riêng của nó về object stream quay lại sai

PDFiumPas từng gặp đúng lỗi này trước v2.16.0. Các object stream được xây dựng với /Predictor 12, trường hợp phổ biến cho các bộ ghi PDF 1.5+, mở rộng qua PdfExpandObjectStreams thành các byte đã lấy hiệu mà bộ quét cấu trúc không thể phân tích, nên catalog, /OutputIntents, và các đối tượng /Metadata đóng gói bên trong về mặt hiệu quả trở nên vô hình với các lượt quét tuân thủ chuẩn — không ngoại lệ, không cảnh báo, chỉ một lượt quét âm thầm hành xử như thể các đối tượng đó không tồn tại. Cơ chế sâu hơn về cách PDFiumPas giải quyết một object stream theo bảng cross-reference đang hoạt động, bao gồm các trường hợp luồng xref lai và thuần túy, được nói đến riêng trong bài viết về xác thực luồng object và xref với PDFiumPas; bước predictor được mô tả ở đây chạy sau lượt giải quyết đó, trên các byte mà mỗi đối tượng nén thực sự chứa

Đảo ngược các hàng Predictor PNG và TIFF trong Pascal

PDFiumPas đảo ngược việc lấy hiệu trong một routine duy nhất, PdfApplyPredictor, và phép toán hình học của nó đáng biết dù bạn gọi nó hay tự triển khai lại ý tưởng trong code Delphi của riêng bạn. Chiều rộng hàng tính bằng byte là ceil(Columns × Colors × BitsPerComponent ÷ 8) và chiều rộng byte cho mỗi pixel mà cả hai thuật toán dùng là ceil(Colors × BitsPerComponent ÷ 8) — làm sai phép làm tròn ở một trong hai và việc tái thiết đọc vượt qua ranh giới hàng thay vì bên trong một hàng. Một /Predictor dưới 2 được để nguyên, vì 1 nghĩa là bộ mã hóa hoàn toàn không áp dụng phép biến đổi nào; 2 chọn nhánh TIFF được hiển thị dưới đây, và bất cứ gì từ 10 trở lên rơi vào tái thiết bộ lọc hàng PNG, nơi byte tag ở đầu mỗi hàng — không phải giá trị /Predictor đã khai báo — quyết định hàng cụ thể đó được hoàn tác như thế nào

function PdfApplyPredictor(const Src: TBytes;
  Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
  RowLen, Bpp, R, I: Integer;
begin
  Result:= Src;
  if Predictor< 2 then
    Exit;                                   // 1 = no prediction, nothing to undo
  if Colors<= 0 then Colors:= 1;
  if Bpc<= 0 then Bpc:= 8;
  if Columns<= 0 then Columns:= 1;
  if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
    Exit;                                   // reject hostile row geometries
  RowLen:= (Columns* Colors* Bpc+ 7) div 8;  // ceil(), per ISO 32000-1 Table 8
  Bpp:= (Colors* Bpc+ 7) div 8;
  if Predictor= 2 then
  begin
    if Bpc<> 8 then
      Exit;                                 // only the 8-bit layout is reconstructed
    Result:= Copy(Src, 0, Length(Src));
    R:= 0;
    while R+ RowLen<= Length(Result) do
    begin
      for I:= R+ Bpp to R+ RowLen- 1 do
        Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
      Inc(R, RowLen);
    end;
    Exit;
  end;
  // Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
  SrcOfs:= R* (RowLen+ 1);
  DstOfs:= R* RowLen;
  Tag:= Src[SrcOfs];
  Inc(SrcOfs);
  for I:= 0 to RowLen- 1 do
  begin
    if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0;   // byte to the left
    if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0;   // byte above
    case Tag of
    1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A);            // Sub
    2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B);            // Up
    3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
    // Paeth (tag 4) adds whichever of A, B or the byte above-left sits
    // closest to the linear predictor A+ B- C; tag 0 (None) copies the
    // filtered byte through unchanged
    else Result[DstOfs+ I]:= Src[SrcOfs+ I];
    end;
  end;
end;

PDFiumPas đã thay đổi gì trong v2.16.0

Bản sửa phát hành trong PDFiumPas v2.16.0 nằm bên trong PdfReadAndDecodeStream, routine đọc các byte thô của một stream và giải mã chúng cho mỗi caller cần kiểm tra cấu trúc PDF ở cấp byte, bao gồm cả việc mở rộng object stream; nó chỉ thử tái thiết sau khi xác nhận /Filter là một FlateDecode trần trụi, không bao giờ là một chuỗi tầng, vì một bộ lọc theo chuỗi không thể được sửa predictor an toàn ở lớp này. Việc đọc /Predictor, /Colors, /BitsPerComponent, và /Columns trở lại từ từ điển stream cũng không cần một bộ phân tích từ điển tổng quát: PdfDictRefNum tìm mỗi khóa bằng tìm kiếm token-tên trực tiếp trong phạm vi byte của đúng một từ điển đó, điều này an toàn ở đây chính xác vì bốn khóa đó không thể lặp lại hay lồng nhau bên trong một từ điển stream đơn lẻ. Cùng lượt tìm kiếm token-tên đó rủi ro hơn nhiều một khi nó trỏ vào một vùng lớn hơn hay ít giới hạn hơn của một file PDF, đó là chủ đề của bài viết đồng hành về phân tích từ điển PDF an toàn

// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
  Inflated:= PdfInflate(Raw);
  Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
  if Predictor>= 2 then
  begin
    PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
    PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
    PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
    Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
  end
  else
    Result:= Inflated;
end;

Trước v2.16.0, một object stream được xây dựng với /Predictor 12 mở rộng thành các byte đã lấy hiệu mà không có lỗi nào được ném ra, nên bất kỳ đối tượng catalog, /OutputIntents, hay /Metadata nào đóng gói bên trong nó biến mất khỏi các lượt quét cấu trúc của PDFiumPas mà không có cảnh báo nào. Sau bản sửa, cùng object stream đó giải nén rồi tái thiết đúng đắn, và các đối tượng đóng gói bên trong nó trở nên hiển thị trở lại với các lượt quét đó. Các giới hạn phòng thủ đi kèm với bản sửa: PdfApplyPredictor giờ từ chối thẳng thừng /Colors trên 64, /BitsPerComponent trên 32, và /Columns trên 2^24, vì các tổ hợp đó mô tả các hình học hàng mà không nhà sản xuất PDF thật nào cần và tồn tại chủ yếu để khiến một bộ giải mã cấp phát nhiều bộ nhớ hơn nhiều so với các byte đầu vào biện minh

var
  Pdf: TPdf;
  Report: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'incoming.pdf';
    Pdf.Active := True;
    Report := Pdf.ValidatePdfA;
    if not Report.IsCompliant then
      LogNonCompliance(Report); // caller-supplied handler
  finally
    Pdf.Free;
  end;
end;

Giới hạn đáng biết

Việc tái thiết predictor của PDFiumPas có hai giới hạn đáng biết trước khi bạn dựa vào nó. Việc tái thiết TIFF Predictor 2 chỉ bao phủ trường hợp 8-bit-mỗi-thành-phần; PDF cho phép đóng gói hẹp hơn, nhưng dữ liệu lấy hiệu kiểu TIFF dưới-một-byte đi qua không được tái thiết thay vì bị đoán mò, nên một stream khai báo /Predictor 2 với /BitsPerComponent 1, 2, hay 4 sẽ không giải mã đúng qua đường này ngày nay. Dự đoán PNG không có giới hạn như vậy — mỗi hàng cung cấp tag bộ lọc riêng của nó, và cả năm loại đã định nghĩa đều được tái thiết bất kể giá trị /Predictor đã khai báo giữa 10 và 15 tình cờ là gì, khớp với cách lọc kiểu PNG thực sự hoạt động: giá trị đã khai báo gần với một gợi ý về những gì bộ mã hóa chủ yếu dùng hơn là một lời hứa về mọi hàng

Engine render gốc của PDFium đã tái thiết đúng dữ liệu ảnh và content-stream đã lấy hiệu bởi predictor, chính xác là lý do vì sao một file có thể render hoàn hảo trong bất kỳ trình xem thông thường nào trong khi một bộ xác thực, bộ ký, hay bộ kiểm tra phiên bản cấp byte xây dựng trên nó đọc sai chính các byte đó. Việc giải mã nhận biết predictor được mô tả ở đây hậu thuẫn các tính năng xác thực PDF/A, quét cấu trúc, và ký của PDFiumPas, component PDFium VCL gốc cho Delphi và C++Builder