Bài viết kỹ thuật

Tạo chú thích đánh dấu văn bản bằng QuadPoints của PDFium trong Delphi

Thành phần PDFium Component tạo các chú thích đánh dấu văn bản (gồm tô sáng, gạch chân, gạch ngang và gạch lượn sóng) thông qua hàm TPdf.CreateAnnotation: bạn thiết lập HasAttachmentPoints := True trên bản ghi TPdfAnnotation và điền tọa độ tứ giác AttachmentPoints của nó, và thành phần này sẽ ghi lại mục nhập QuadPoints được định nghĩa trong ISO 32000-1 §12.5.6.10. Đó là toàn bộ giao diện API. Lý do bài viết này ra đời là do những gì diễn ra ngầm bên dưới, bởi vì chuỗi cuộc gọi PDFium gốc có một chế độ lỗi hiển thị một triệu triệu chứng mơ hồ nhất: hàm FPDFAnnot_SetAttachmentPoints luôn trả về false trên một chú thích mới được tạo, không đi kèm mã lỗi hay bất kỳ gợi ý nào. Đây là phần bổ trợ ở phía tạo chú thích cho biết về đọc và xem lại các chú thích hiện có, vốn đi theo hướng ngược lại của cùng các cấu trúc này

Kịch bản gỡ lỗi luôn diễn ra giống nhau. Bạn tạo một chú thích tô sáng, gọi hàm thiết lập điểm đính kèm với chỉ số 0, hàm trả về false, và bạn bắt đầu nghi ngờ các tọa độ của mình. Bạn đảo vị trí các điểm, lật ngược trục Y, chuyển đổi không gian trang sang không gian thiết bị. Không có cách nào hiệu quả, vì tọa độ chưa bao giờ là nguyên nhân. Vấn đề thực chất nằm ở ngữ nghĩa chỉ số (index semantics) của C API, và một khi bạn nhận ra điều đó, giải pháp sửa lỗi chỉ mất hai dòng mã

Ý nghĩa của QuadPoints trong tiêu chuẩn ISO 32000-1

QuadPoints là một mảng gồm 8×n số mô tả n hình tứ giác, và mục ISO 32000-1 §12.5.6.10 yêu cầu thuộc tính này trên mọi chú thích đánh dấu văn bản: mỗi hình tứ giác đánh dấu một từ hoặc nhóm các từ liền kề mà thao tác tô sáng, gạch chân hoặc gạch ngang áp dụng lên đó. Mục nhập Rect của chú thích vẫn tồn tại, nhưng đối với các kiểu con đánh dấu (markup subtypes), nó chỉ đóng vai trò bao quanh vùng chọn; các tứ giác (quads) mới là những gì trình kết xuất thực sự vẽ lên. Chúng ta sử dụng hình tứ giác thay vì hình chữ nhật vì văn bản có thể bị xoay hoặc bị nghiêng, do đó bốn góc được lưu trữ dưới dạng bốn điểm độc lập: x1 y1 x2 y2 x3 y3 x4 y4

Thứ tự của bốn điểm đó là nơi đặc tả kỹ thuật và triển khai thực tế có sự khác biệt. Văn bản đặc tả mô tả các điểm vẽ theo hình tứ giác ngược chiều kim đồng hồ, nhưng trình kết xuất của chính Adobe từ trước đến nay luôn diễn giải chúng theo dạng chữ Z: trước tiên là cạnh trên từ trái sang phải, sau đó là cạnh dưới từ trái sang phải. Vì mọi nhà phát triển đều kiểm thử với Acrobat, nên hầu như mọi trình kết xuất (bao gồm cả PDFium) đều tuân theo dạng chữ Z, và các tệp tin tuân theo câu chữ mô tả của đặc tả kỹ thuật lại hiển thị thành các vệt tô sáng bị co cụm hoặc bị vặn xoắn trong một số trình xem. Cấu trúc FS_QUADPOINTSF của PDFium mã hóa chính xác quy ước này: (x1,y1) là góc trên bên trái, (x2,y2) là góc trên bên phải, (x3,y3) là góc dưới bên trái, (x4,y4) là góc dưới bên phải, tính theo tọa độ trang với trục Y hướng lên trên. Hãy tuân thủ thứ tự đó; các trình kết xuất có thể khoan dung với nhiều thứ, nhưng một tứ giác bị xáo trộn tọa độ thì chắc chắn không nằm trong số đó

Tại sao FPDFAnnot_SetAttachmentPoints lại trả về false?

Hàm FPDFAnnot_SetAttachmentPoints thất bại trên một chú thích mới vì hợp đồng thiết kế của nó là để thay thế hình tứ giác tại một chỉ số cho trước, trong khi một chú thích mới được tạo có số lượng hình tứ giác bằng không. Chữ ký hàm nhận một handle chú thích, một chỉ số quad_index, và các điểm tọa độ; chỉ số 0 không có nghĩa là "ngăn chứa đầu tiên, tạo mới nếu cần", nó có nghĩa là "tứ giác số 0 đang tồn tại", và khi hàm FPDFAnnot_CountAttachmentPoints báo cáo kết quả là 0, không có tứ giác nào như vậy và cuộc gọi trả về false. Hàm dùng để tạo mới một ngăn chứa là FPDFAnnot_AppendAttachmentPoints. Mọi chú thích được tạo qua FPDFPage_CreateAnnot đều bắt đầu với số lượng bằng không, vì vậy đường dẫn tạo mới bắt buộc phải gọi Append trước, và chỉ các lượt cập nhật sau đó mới được phép gọi Set

Lỗi này đã ảnh hưởng đến chính PDFium Component. Cho đến phiên bản v1.79.0, quy trình nội bộ dùng chung bởi CreateAnnotationSetAnnotation đã cố định dòng mã FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), điều này chỉ đúng khi cập nhật một chú thích đánh dấu hiện có và chắc chắn thất bại đối với chú thích mới tạo, hiển thị dưới dạng ngoại lệ EPdfException với thông điệp 'Cannot set attachment points'. Bản sửa lỗi được phát hành trong phiên bản v1.79.1 đã phân nhánh dựa trên số lượng đếm được

// Inside the component's annotation writer (v1.79.1+):
// a new annotation has no quad slots yet, so Append creates
// the first one; Set only replaces a slot that already exists
if FPDFAnnot_CountAttachmentPoints(Annotation) = 0 then
  Check(FPDFAnnot_AppendAttachmentPoints(Annotation, QuadPoints) <> 0,
    'Cannot set attachment points')
else
  Check(FPDFAnnot_SetAttachmentPoints(Annotation, 0, QuadPoints) <> 0,
    'Cannot set attachment points');

Mô hình tương tự cũng áp dụng nếu bạn gọi trực tiếp các hàm C được xuất ra, điều mà thành phần này cho phép bạn thực hiện vì tất cả các điểm truy cập FPDFAnnot_* đều được hiển thị trong PDFium.pas. Bất cứ khi nào bạn nắm giữ một handle FPDF_ANNOTATION và muốn ghi dữ liệu tứ giác, hãy truy vấn hàm FPDFAnnot_CountAttachmentPoints trước và điều hướng xử lý tương ứng. Nếu bạn đang tìm kiếm lý do tại sao "FPDFAnnot_SetAttachmentPoints trả về false", nhánh kiểm tra-số-lượng-sau-đó-append này gần như chắc chắn là câu trả lời của bạn

Tạo một vệt tô sáng bằng TPdf.CreateAnnotation

Nhờ thành phần tự động xử lý phân nhánh Append và Set cho bạn, việc tạo một vệt tô sáng chỉ đơn giản là điền thông tin vào một bản ghi. Ví dụ dưới đây tạo một trang A4 và vẽ một vệt tô sáng màu vàng bán trong suốt trên một vùng kích thước 200×20 point; lưu ý rằng tứ giác tuân theo thứ tự chữ Z được mô tả ở trên, và thuộc tính Rectangle được thiết lập để bao quanh tứ giác, giúp các trình xem thực hiện kiểm tra va chạm đối chiếu với Rect hoạt động một cách hợp lý

var
  Pdf: TPdf;
  A: TPdfAnnotation;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);

    FillChar(A, SizeOf(A), 0);
    A.Subtype := anHighlight;
    A.HasColor := True;
    A.Color := clYellow;
    A.ColorAlpha := $80;                     // 50% opacity
    A.HasAttachmentPoints := True;
    A.AttachmentPoints[1].X := 50;  A.AttachmentPoints[1].Y := 700; // top-left
    A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // top-right
    A.AttachmentPoints[3].X := 50;  A.AttachmentPoints[3].Y := 680; // bottom-left
    A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // bottom-right
    A.Rectangle.Left := 50;  A.Rectangle.Top := 700;
    A.Rectangle.Right := 250; A.Rectangle.Bottom := 680;
    A.ContentsText := 'Highlighted region';
    Pdf.CreateAnnotation(A);

    Pdf.SaveAs('highlighted.pdf');
  finally
    Pdf.Free;
  end;
end;

Việc chuyển đổi các kiểu con chú thích chỉ tốn một dòng mã. Các kiểu anUnderline, anStrikeout, và anSquiggly có cấu trúc bản ghi giống hệt nhau (bao gồm cả các điểm tứ giác), bởi vì tiêu chuẩn ISO 32000-1 coi cả bốn kiểu này thuộc cùng một nhóm chú thích và chỉ khác biệt ở cách trang trí vùng chọn tứ giác. Các kiểu con không phải là đánh dấu văn bản, ví dụ anSquare, anCircle, và anText, tự định vị vị trí chỉ dựa vào thuộc tính Rectangle; hãy để thuộc tính HasAttachmentPoints là False đối với các kiểu đó, và cơ chế xử lý tứ giác sẽ không hoạt động

Tại sao dòng mã AttachmentPoints[0] biên dịch thành công trong Delphi nhưng thất bại trong FPC?

Kiểu TQuadrilateralPoint được khai báo dưới dạng array [1..4] of TPdfPoint, một mảng bắt đầu từ chỉ số 1 (1-based array), và điều này dễ làm vấp ngã bất kỳ ai quen gõ chỉ số bắt đầu từ 0. Viết dòng mã A.AttachmentPoints[0] và trình biên dịch dcc32 của Delphi sẽ biên dịch nó mà không đưa ra lời phàn nàn nào, vì tính năng kiểm tra phạm vi chỉ số (range checking) bị tắt theo mặc định; khi chạy chương trình, biểu thức này sẽ âm thầm đọc hoặc ghi vào vùng nhớ nằm ngay trước mảng (trong cấu trúc bản ghi TPdfAnnotation là một trường liền kề). Vệt tô sáng của bạn sẽ có một góc mang tọa độ rác, hoặc một trường liền kề bị ghi đè dữ liệu hỏng mà không có cảnh báo nào được đưa ra. Trình biên dịch Free Pascal đã phát hiện chính xác lỗi này trong các tệp mã nguồn mẫu của chúng tôi trong quá trình chuyển cổng sang Lazarus: fpc thực hiện kiểm tra phạm vi chỉ số tại thời điểm biên dịch đối với các chỉ số không đổi và đã từ chối thẳng thừng biểu thức AttachmentPoints[0..3], đó là cách mà lỗi lệch chỉ số và lỗi thư viện Set-so-với-Append được phát hiện cùng nhau

Lấy tọa độ tứ giác từ văn bản thực tế

Các hình chữ nhật cố định tọa độ (hardcoded) là đủ tốt cho một bản thử nghiệm, nhưng các vệt tô sáng trong thực tế phải bám sát các ký tự thực, và tọa độ nên được lấy từ hình học trang văn bản của PDFium chứ không phải từ sự phỏng đoán. Các quy trình được trình bày trong tài liệu hướng dẫn trích xuất văn bản bằng PDFium Component cung cấp các hộp bao quanh (bounding boxes) cho từng ký tự trong cùng một không gian tọa độ trang mà tứ giác sử dụng, do đó một kết quả tìm kiếm khớp sẽ chuyển đổi trực tiếp thành các góc tọa độ: phía bên trái của ký tự đầu tiên, phía bên phải của ký tự cuối cùng, đỉnh và đáy từ kích thước của dòng. Nếu bạn đang tự tạo văn bản và cần biết các dòng sẽ nằm ở đâu trước khi chúng được hiển thị, bài viết về đo lường văn bản và ngắt dòng sẽ hướng dẫn tính toán trước các kích thước đó

Một giới hạn thực tế cần chia sẻ: bản ghi TPdfAnnotation chỉ mang một cấu trúc TQuadrilateralPoint duy nhất, do đó một lệnh gọi CreateAnnotation chỉ ghi lại một hình tứ giác duy nhất. Một vùng chọn trải dài qua ba dòng sẽ cần đến ba hình tứ giác (mỗi dòng một hình theo mục §12.5.6.10), và bạn có hai giải pháp để thực hiện điều này. Cách đơn giản là tạo một chú thích cho mỗi dòng, cách này hiển thị chính xác ở mọi nơi và giữ nguyên việc sử dụng API cấp thành phần. Cách tối giản hơn là một chú thích duy nhất mang cả ba hình tứ giác, nghĩa là bạn tạo chú thích thông qua thành phần và sau đó tự mình gọi hàm xuất bản FPDFAnnot_AppendAttachmentPoints cho hình tứ giác thứ hai và thứ ba, cơ chế này hoạt động chính xác vì Append tạo mới ngăn chứa chứ không thay thế. Đừng cố gắng tạo nhiều hình tứ giác bằng cách lặp lại các lệnh gọi SetAttachmentPoints; mọi chỉ số vượt quá số lượng hiện tại đều sẽ trả về false, vì cùng một lý do giống như chỉ số 0 trên chú thích mới tạo

Sau khi ghi dữ liệu, hãy xác thực kết quả trong một trình xem thực tế thay vì chỉ tin tưởng vào các mã trả về: mở tệp tin bằng Acrobat hoặc bất kỳ trình xem nào dựa trên PDFium và xác nhận xem phần đánh dấu có khớp đúng vị trí văn bản, hiển thị ở độ mờ mong muốn, và tồn tại nguyên vẹn qua chu trình lưu và tải lại hay không. Các kiểu chú thích, cơ chế xử lý tứ giác và trình viết chú thích nhận biết số lượng đếm được trình bày ở đây đều là một phần của sản phẩm tiêu chuẩn PDFium Component dành cho Delphi, C++Builder, và Lazarus; trang sản phẩm cung cấp đầy đủ tài liệu tham khảo API chú thích bên cạnh các tính năng khác của thư viện