Bài viết kỹ thuật

Handle page object PDFium lỗi thời sau transform trong Delphi

Khi FPDFPage_TransFormWithClip viết lại một trang, mọi handle FPDF_PAGEOBJECT bạn đang giữ vẫn mô tả lần phân tích từ trước khi transform. PDFium Component cho Delphi và C++Builder giải quyết điều này bên trong TransformPageContent, hàm này bỏ tải text page, transform nội dung, rồi tải lại trang để các truy vấn sau đó thấy tọa độ mới

Triệu chứng này im lặng. Bạn áp một tỷ lệ 0,9 để thêm lề in, rồi đọc PageObjectInfo và nhận được đúng những con số bạn đã có trước lệnh gọi. Không exception, không mã lỗi, không gì trong log. Đây là một thất bại khác với text page bị cache được mô tả trong bài về text page lỗi thời sau khi sửa: ở đó cache là một handle FPDF_TEXTPAGE duy nhất bạn có thể bỏ đi và xây lại, ở đây vấn đề là mọi handle page object trong biến của chính bạn, cộng với một lớp getter báo cáo thất bại qua một mã trả về mà hầu hết bên gọi vứt bỏ

Vì sao biên page object trở nên lỗi thời mà không có lỗi?

Vì một handle page object là một con trỏ vào một biểu diễn đã phân tích của một content stream cụ thể, và một transform toàn trang thay thế content stream đó bằng một cái mới. PDFium không duyệt call stack của bạn để tìm handle cần vá. Nó xây một đồ thị object mới tinh và để lại cái cũ y nguyên như nó vốn có, nên một lần đọc trên handle cũ là một lần đọc hoàn toàn hợp lệ trên một cấu trúc không còn tương ứng với những gì file nói nữa

ISO 32000-1 §7.8.2 định nghĩa content stream là chuỗi toán tử vẽ một trang, và §8.3.3 định nghĩa cách ma trận biến đổi hiện hành ánh xạ user space lên device space. Một transform mức trang được biểu diễn bằng cách bọc và viết lại những toán tử đó, không phải bằng cách sửa tọa độ theo từng object tại chỗ. Vậy nên tọa độ mà các object mang có thể hoàn toàn không đổi; thứ thay đổi là ma trận đang có hiệu lực khi chúng được vẽ. Bất kỳ handle nào được phân tích dưới ma trận cũ trả lời các câu hỏi hình học dưới ma trận cũ, và trả lời chúng mà không phàn nàn

FPDFPage_TransFormWithClip thực sự viết lại gì

Nó viết lại trang, không phải các snapshot của bạn. FPDFPage_TransFormWithClip nhận một FS_MATRIX và một hình chữ nhật clip FS_RECTF và áp cả hai lên toàn bộ nội dung trang. Đó là lệnh gọi đúng cho lề, tỷ lệ dàn trang (imposition), và chuẩn hóa một trang có kích thước bất thường theo một khung mục tiêu. Đó là lệnh gọi sai nếu bạn kỳ vọng các handle hiện có sẽ tự cập nhật theo, và cũng đáng nhớ rằng nó chỉ chạm vào nội dung trang: chú thích là một lớp riêng biệt và cần TransformPageAnnotations, hàm này chuyển tiếp cùng sáu hệ số ma trận cho FPDFPage_TransformAnnots

var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // snapshot taken before the transform

  Scale.a:= 0.9;   Scale.b:= 0.0;
  Scale.c:= 0.0;   Scale.d:= 0.9;
  Scale.e:= 29.7;  Scale.f:= 42.0;        // 5% margin, A4 in points
  Clip:= Pdf.GetPageBox(pbMedia);
  Pdf.TransformPageContent(Scale, Clip);

  // Info.Bounds still holds pre-transform geometry, and Info.Handle now
  // points into a page that TransformPageContent has already replaced
end;

Thứ tự làm mới mà TransformPageContent dùng

Bốn bước, theo thứ tự này: bỏ tải text page, transform, sinh nội dung, tải lại trang. TPdf.TransformPageContent chạy chính xác chuỗi đó. Nó gọi CheckPageActive, copy ma trận và clip vào dạng bản ghi native của chúng, gọi UnloadTextPage, rồi FPDFPage_TransFormWithClip, rồi UpdatePage, wrapper quanh FPDFPage_GenerateContent, và cuối cùng ReloadPage

Mỗi bước đều xứng đáng có mặt. UnloadTextPage đi trước vì FPDF_TEXTPAGE đã cache giữ các box ký tự được tính dưới ma trận cũ, và nó cũng loại bỏ danh sách web-link dẫn xuất cùng bất kỳ phiên tìm kiếm đang diễn ra nào được xây từ nó. FPDFPage_GenerateContent phải chạy trước khi tải lại, vì transform sống trong trang trong bộ nhớ cho đến khi nó được serialize trở lại vào content stream, và một lần tải lại nếu không sẽ phân tích lại stream chưa sửa đổi. ReloadPage kết thúc bằng FPDF_LoadPage đối chiếu với chỉ số trang hiện tại, đó là thứ duy nhất thực sự cho bạn một đồ thị object mới

// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // unload text page, transform,
                                           // generate content, reload page
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // handle and bounds from the new parse
    if Info.Bounds.Right> PageWidth then
      Log('object '+ IntToStr(I)+ ' still overflows after scaling');
  end;
end;

Một chi tiết trong ReloadPage đáng sao chép lại nếu bạn từng tự viết chuỗi này. Nó tải trang mới trước và chỉ commit nó vào trường sau đó, nên một lần tải trang thất bại để lại trang native hiện tại và mọi cache dẫn xuất của nó nguyên vẹn thay vì đẩy bạn vào trạng thái tháo dỡ nửa vời. Tải lại không miễn phí — bạn đang trả giá cho một lần phân tích lại đầy đủ trang — nhưng nó chỉ trả một lần cho mỗi transform, không phải một lần cho mỗi truy vấn, và không có phương án đúng nào rẻ hơn

Đừng mang handle qua lần tải lại

Sau khi tải lại, các handle cũ không chỉ đơn thuần lỗi thời, chúng lơ lửng (dangling). FPDF_PAGE trước đó đã đóng, và các giá trị FPDF_PAGEOBJECT thuộc về nó là các con trỏ vào bộ nhớ đã giải phóng. TPdfPageObjectInfo phơi bày handle native trong trường Handle của nó, thực sự hữu ích để truyền một object thẳng vào một lệnh gọi mức thấp hơn, và cũng thực sự nguy hiểm nếu giữ nó trong một trường form hoặc một danh sách qua một thao tác tải lại trang. Coi một bản ghi snapshot chỉ hợp lệ cho đến lệnh gọi tiếp theo tái sinh nội dung, cùng tinh thần với các quy tắc sở hữu được thảo luận trong bài về ABI và an toàn bộ nhớ tại ranh giới PDFium

Một getter có thể thất bại mà vẫn trông giống dữ liệu hợp lệ không?

Có, và đây là nửa thứ hai của cùng vấn đề. FPDFPageObj_GetRotatedBoundsFPDFPageObj_GetIsActive là các getter tham số ra: chúng trả về một cờ thành công int và ghi câu trả lời thật vào một tham số tham chiếu. Cả hai có thể trả về FALSE cho một object đã được tạo nhưng trang của nó chưa được phân tích lại. Khi điều đó xảy ra, tham số ra không bị đụng tới, và một record Pascal khởi tạo với Default(TPdfPageObjectInfo) toàn số không, nên bên gọi thấy một tứ giác với bốn điểm tại gốc tọa độ và một cờ Active là False. Một lệnh gọi thất bại đã âm thầm được thăng cấp thành dữ liệu trông có vẻ hợp lý

TPdfPageObjectInfo trả lời điều này bằng các sentinel tường minh. HasRotatedBounds mang kết quả của lệnh gọi FPDFPageObj_GetRotatedBounds, HasActiveState mang kết quả của FPDFPageObj_GetIsActive, và các trường hình học và trạng thái chỉ được ghi khi sentinel tương ứng là True. Cùng hình dạng đó lặp lại khắp record cho các getter tham số ra khác, nên HasMatrix, HasFillColor, HasStrokeColor, và HasStrokeWidth đều có cùng ý nghĩa: lệnh gọi native đã thành công và trường lân cận có ý nghĩa

Info:= Pdf.PageObjectInfo(I);

if Info.HasRotatedBounds then
  // RotatedBounds is array [1..4] of TPdfPoint, in draw order
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // the native call failed; fall back to the axis-aligned rectangle
  UseRect(Info.Bounds);

if Info.HasActiveState and (not Info.Active) then
  SkipObject(I);         // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive

Khuôn mẫu này tổng quát hóa cho mọi getter PDFium theo quy ước mã trả về-cộng-tham-số-ra, và có rất nhiều cái như vậy. Nếu một wrapper gộp quy ước đó thành một kết quả hàm trơn, nó đã vứt bỏ tín hiệu duy nhất phân biệt "câu trả lời là số không" với "không có câu trả lời". Mang thêm một boolean cho mỗi trường tốn một byte và loại bỏ hẳn một loại lỗi nơi một record mặc định bị nhầm thành một phép đo

Những nơi vấn đề này vẫn cắn bạn

Ba giới hạn trung thực. Thứ nhất, việc làm mới là theo từng trang: transform trang hai và bất kỳ handle nào bạn đang giữ cho trang một không bị ảnh hưởng, nhưng giờ bạn có hai trang được phân tích ở các thời điểm khác nhau và bạn phải tự nhớ snapshot nào đến từ trang nào. Thứ hai, tính ổn định của chỉ số không được đảm bảo qua một lần tái sinh nội dung — sau khi tải lại, chỉ số 3 là bất cứ gì chỉ số 3 tình cờ là trong lần phân tích mới, nên hãy tái nhận diện object theo kiểu và hình học của chúng thay vì giả định vị trí giữ nguyên. Thứ ba, hình chữ nhật clip trong FPDFPage_TransFormWithClip được áp lên nội dung trang và không thay đổi kích thước bất kỳ box trang nào; nếu bạn thu nhỏ nội dung để tạo lề, MediaBox vẫn giữ kích thước như trước, và một trình xem sẽ hiển thị tờ giấy gốc với bản vẽ bị co lại bên trong nó. Không điều nào trong số này kỳ lạ — đó là hệ quả bình thường của một API C trao con trỏ vào trạng thái đã phân tích và để vòng đời cho bên gọi. Cách sửa là cách hoạt động ở mọi nơi khác: định nghĩa chính xác khi nào một snapshot hết hạn, làm mới tại ranh giới đó, và không bao giờ để một lệnh gọi thất bại giả trang thành một giá trị

Nếu bạn đang tìm hiểu hành vi ma trận nói chung hơn, thứ tự nhân quyết định nơi một transform hạ cánh được trình bày trong bài về prepend, append và pivot với ma trận. Các API transform và page object mô tả ở đây đi kèm trong PDFium Component cho Delphi và C++Builder, trang sản phẩm của nó mang tài liệu tham chiếu đầy đủ cho record snapshot page object và các trường sentinel của nó