Bài viết kỹ thuật

Lỗi xoay kép và fit-zoom của PDFium trong Delphi

Hàm FPDF_RenderPageBitmap của PDFium Component nhận một tham số rotate mà PDFium luôn cộng thêm lên trên bất cứ phép xoay nào trang đã mang sẵn trong entry /Rotate riêng của nó, nên đọc phép xoay đã lưu của một trang và đưa chính giá trị đó trở lại lệnh gọi render xoay trang đó hai lần. Cùng một lỗi giống hệt xuất hiện trong phép toán fit-zoom: định kích thước một thumbnail từ chiều rộng và chiều cao chưa xoay của trang tạo ra tỉ lệ khung hình sai bất cứ khi nào /Rotate là 90 hoặc 270 độ, vì bitmap đã render ra với chiều rộng và chiều cao bị hoán đổi

Lỗi này dễ nhận ra một khi bạn biết cần tìm gì, và dễ bỏ sót cho đến lúc đó. Một lô hóa đơn scan đến với hỗn hợp bản gốc chân dung và ngang, ai đó nắn thẳng một nửa trong số đó bằng một phép xoay 90 độ trong Acrobat trước khi lưu trữ, và dải thumbnail trong một trình xem Delphi xây dựng trên PDFium render những trang cụ thể đó nghiêng, lộn ngược, hay bị nén vào một hộp có hình dạng cho sai hướng. Không có gì ném ra ngoại lệ. Không có gì ghi log lỗi. Các pixel đơn giản là sai, và chỉ với tập con các trang ai đó đã xoay sau này — chính xác là loại lỗi sống sót qua một lượt QA đầy đủ trên một PDF test chưa xoay và rồi lộ ra trong sản xuất ở trang 47 của một PDF thật

Vì sao PDFium xoay trang hai lần?

PDFium tự động áp dụng giá trị /Rotate riêng của một trang mỗi lần nó render một bitmap, bất kể thứ gì được truyền cho bộ render. Tham số rotate của FPDF_RenderPageBitmap, phơi bày trong PDFiumPas như các giá trị TRotation ro0, ro90, ro180 và ro270 trên TPdf.RenderPage, TPdf.RenderTile và TPdf.RenderPageThumbnail, không đặt góc mà một trang nên kết thúc ở đó; tham số rotate đặt lượng xoay thêm nào để xếp lên trên bất cứ thứ gì từ điển trang đã chỉ định, đó là lý do vì sao mỗi phương thức trong số đó mặc định nó là ro0

TPdf.PageRotation đọc chính giá trị /Rotate đó thông qua FPDFPage_GetRotation, và code ứng dụng thường cần nó vì những lý do không liên quan gì đến việc render, chẳng hạn quyết định cách bố trí một chú thích trong không gian trang. Cái bẫy là một dòng duy nhất: truyền PageRotation vào tham số Rotation của RenderPage, kỳ vọng lệnh gọi chuẩn hóa trang về thẳng đứng. Một trang đã lưu với /Rotate 90 hiển thị đúng, đã xoay, trong bất kỳ trình xem tuân thủ chuẩn nào, kể cả PDFium; thêm ro90 lần nữa lên trên đó và trang xoay đến 180 độ thay vì 90 độ dự định, trong khi một trang không có phép xoay nào cả bị xoay thêm một phần tư vòng không mong muốn mà không có lý do

// Wrong: PageRotation already reflects /Rotate, and PDFium applies
// it automatically on every render -- passing it again as Rotation
// doubles the angle
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, Pdf.PageRotation, []);

// Right: leave Rotation at its ro0 default and let PDFium apply the
// page's own /Rotate exactly once
Bitmap := Pdf.RenderPage(0, 0, TargetW, TargetH, ro0, []);

Tham số Rotation thực sự dùng để làm gì

Tham số Rotation xứng đáng có chỗ đứng trong API cho một công việc thực sự khác: thêm một phép xoay chỉ-để-xem không liên quan gì đến hướng đã lưu của một trang, kiểu mà một nút toolbar xoay-view áp dụng mà không đụng đến file bên dưới. TPdfView giữ hai khái niệm này như hai thuộc tính riêng biệt chính vì lý do đó. TPdfView.PageRotation phản chiếu /Rotate riêng của trang và, thông qua FPDFPage_SetRotation, có thể ghi một giá trị mới trở lại vào tài liệu; TPdfView.Rotation là một thuộc tính tạm thời, chỉ-để-xem mặc định là ro0 và không bao giờ đụng đến file. Đọc thuộc tính đầu tiên và ghi nó vào thuộc tính thứ hai là toàn bộ lỗi trong một câu

// View-only: rotates what the user sees, changes nothing in the file
procedure TViewerForm.RotateViewClick(Sender: TObject);
begin
  case PdfView.Rotation of
    ro0:   PdfView.Rotation := ro90;
    ro90:  PdfView.Rotation := ro180;
    ro180: PdfView.Rotation := ro270;
    ro270: PdfView.Rotation := ro0;
  end;
end;

// Persistent: rewrites the page's own /Rotate entry in the document
procedure TViewerForm.RotatePageClick(Sender: TObject);
begin
  case PdfView.PageRotation of
    ro0:   PdfView.PageRotation := ro90;
    ro90:  PdfView.PageRotation := ro180;
    ro180: PdfView.PageRotation := ro270;
    ro270: PdfView.PageRotation := ro0;
  end;
end;

Vì sao việc định kích thước fit-zoom hỏng theo cùng cách?

Việc định kích thước fit-zoom hỏng vì một lý do hình ảnh gương: phép tính bắt đầu từ sai cặp số thay vì sai góc. Một cách điển hình để định kích thước một hộp thumbnail là hỏi PDFium chiều rộng và chiều cao của một trang, so sánh tỉ lệ khung hình đó với hộp có sẵn, và tính hình chữ nhật lớn nhất vừa bên trong nó — hoạt động sạch sẽ với một trang chưa xoay. Cùng phép tính đó âm thầm thất bại với một trang /Rotate 90 hoặc /Rotate 270 khi chiều rộng và chiều cao đến từ một lệnh gọi báo cáo kích thước nội tại, chưa xoay của trang: một trang chân dung A4 mang /Rotate 90 vẫn báo cáo khoảng 595 x 842 điểm, dù PDFium render nó, đúng đắn, ở khoảng 842 x 595 một khi phép xoay có hiệu lực, và một hộp fit tính từ cặp chưa xoay kết thúc với hình dạng hoàn toàn sai hướng

FPDF_GetPageSizeByIndex là một ví dụ cụ thể của một lệnh gọi báo cáo kích thước nội tại, chưa xoay đó theo thiết kế, khiến nó tiện lợi cho việc quét kích thước trang mà không cần nạp mọi trang và rủi ro cho phép toán fit-zoom quên tính đến điều đó. Cách khắc phục đi thẳng từ việc nêu tên vấn đề: kiểm tra phép xoay của trang trước khi làm phép toán fit, hoán đổi chiều rộng và chiều cao bất cứ khi nào phép xoay đó là 90 hoặc 270 độ, tính hộp fit từ cặp đã hoán đổi, và vẫn truyền ro0 cho lệnh gọi render thực tế, vì PDFium vẫn là bên áp dụng phép xoay thật

Lấy đúng thumbnail mà không cần phát minh lại phép toán fit

TPdf.RenderPageThumbnail đã mang sẵn bản sửa này, nên con đường ngắn nhất đến một thumbnail đúng là gọi nó thay vì tự lắp ráp lại logic fit-và-xoay bằng tay. Với một chỉ số trang bắt đầu từ 1 và chiều rộng, chiều cao tối đa, RenderPageThumbnail tính một hộp fit, sửa nó cho một /Rotate 90 hoặc 270 nội bộ, và trả về một bitmap do caller sở hữu mà không làm xáo trộn trang hiện tại của tài liệu hay phát ra một sự kiện OnPageChange — điều này quan trọng cho một dải thumbnail được xây dựng cạnh một trình xem sống trên cùng instance TPdf

// PageW, PageH are a page's own (unrotated) dimensions in points, for
// example from FPDF_GetPageSizeByIndex, which reports size before
// /Rotate is applied
function FitBox(PageW, PageH: Double; Rotation: TRotation;
  MaxW, MaxH: Integer; out FitW, FitH: Integer): Boolean;
var
  PgW, PgH, Swap: Integer;
begin
  PgW := Round(PageW);
  PgH := Round(PageH);
  if PgW < 1 then PgW := 1;
  if PgH < 1 then PgH := 1;

  if Rotation in [ro90, ro270] then
  begin
    Swap := PgW;
    PgW := PgH;
    PgH := Swap;
  end;

  Result := (MaxW > 0) and (MaxH > 0);
  if not Result then
    Exit;

  if PgW * MaxH > PgH * MaxW then
  begin
    FitW := MaxW;
    FitH := (MaxW * PgH) div PgW;
  end
  else
  begin
    FitH := MaxH;
    FitW := (MaxH * PgW) div PgH;
  end;
end;

Hàm hỗ trợ FitBox vẫn đáng giữ lại, vì RenderPageThumbnail chỉ bao phủ trường hợp một-bitmap-đơn. Một lưới thumbnail tùy chỉnh, một dải xem trước khi in, hay một hộp thoại chọn trang bố trí nhiều trang theo các hộp độc lập cần cùng phép toán fit nhận biết xoay mà không nhất thiết muốn một bitmap mới cho mỗi ô, và các chế độ zoom fit-page và fit-width riêng của TPdfView dựa vào chính ý tưởng đó nội bộ, chọn giữa chiều rộng và chiều cao của một trang cho phép tính tỉ lệ zoom dựa trên phép xoay hiện tại của view trước khi so sánh nó với vùng client sẵn có. Nếu hiệu năng zoom và cuộn trong loại trình xem đó là vấn đề tiếp theo trong danh sách, bài viết đồng hành về cache render và zoom mượt mà trong một trình xem Delphi dựa trên PDFium tiếp tục đúng nơi việc định kích thước đúng dừng lại

Phát hiện một lần xoay kép trước khi một khách hàng làm điều đó

Một lần xoay kép có một dấu hiệu thị giác đáng tin cậy: một trang được xoay 90 độ trên đường vào ra trông như đã xoay 180 độ so với phần còn lại của tài liệu, không phải 90 độ, vì ro90 thêm vào đã xếp chồng lên ro90 riêng của trang thay vì thay thế nó. Một fixture test chỉ được xây dựng từ các trang /Rotate 0 sẽ không bao giờ bắt được điều này, vì thêm ro0 vào ro0 vẫn là ro0 và lỗi vẫn vô hình; một fixture cần ít nhất một trang được lưu với /Rotate 90 và một trang với /Rotate 270 trước khi một đường code thumbnail hay fit-zoom có thể được tin tưởng

Pipeline trang-thành-bitmap cơ bản được nói đến trong render trang PDF thành JPEG với PDFium Component đã render đúng các trang đã xoay mà không cần code trường hợp đặc biệt nào, chính xác vì nó để Rotation ở mặc định ro0 và để PDFium tự áp dụng /Rotate. Lỗi xoay kép chỉ xuất hiện một khi code ứng dụng bắt đầu đọc lại PageRotation và đưa nó vào một nơi nó không thuộc về

Các lệnh gọi render nhận biết xoay và việc định kích thước thumbnail được mô tả ở đây là một phần của PDFium Component dành cho Delphi và C++Builder, cùng với phần còn lại của các API render, xem, và trích xuất văn bản được xây dựng trên cùng các lớp TPdf và TPdfView