Technical Article

Đo văn bản PDF cho bố cục và xuống dòng trong Delphi

Lời gọi đặt văn bản lên trang PDF thì đơn giản. Bạn cho AddText một chuỗi, một font, một kích thước và một vị trí, và các glyph xuất hiện. Những gì nó không làm là cho bạn biết chuỗi đó sẽ rộng bao nhiêu sau khi được vẽ, và nó không ngắt một chuỗi dài qua nhiều dòng. Một lời gọi đơn vẽ một đoạn văn bản tại một vị trí. Nếu đoạn đó rộng hơn cột bạn muốn nó vừa, nó đơn giản chạy qua cạnh, và không có gì trong lời gọi vẽ cảnh báo bạn. Ngay khi bạn muốn một đoạn văn thay vì một nhãn đơn, phần còn thiếu là chiều rộng của một chuỗi trong font và kích thước đã chọn, được đo trước khi bạn commit nó lên trang

Đây là vấn đề bố cục cổ điển. Để bọc một đoạn văn vào một cột, bạn phải biết, từng từ một, mỗi dòng ứng viên sẽ chiếm bao nhiêu không gian ngang, và bạn phải biết điều đó trước khi vẽ bất cứ điều gì. Xuống dòng là một vòng lặp đo lường bọc quanh một lời gọi vẽ, và một binding chỉ vẽ cho bạn nửa thứ hai. Hỗ trợ đo văn bản trong PDFium component đóng khoảng trống đó với hai hàm, MeasureTextMeasureTextWidth, báo cáo phạm vi được kết xuất của một chuỗi mà không đặt bất kỳ dấu vết nào lên trang

Tại sao đo lường là class helper, không phải phương thức mới trên TPdf

Hỗ trợ đo lường đến như một Delphi class helper cho TPdf, sống trong unit riêng của nó, thay vì là các phương thức mới được gắn vào class TPdf. Một class helper là tính năng ngôn ngữ cho phép bạn gắn các phương thức vào một kiểu hiện có từ bên ngoài khai báo của nó. Khi unit đó nằm trong phạm vi, các phương thức mới được gọi chính xác như thể chúng thuộc về class, vì vậy một phương thức helper đọc như Pdf.MeasureTextWidth(...) mà không cần đối tượng riêng để xây dựng hay truyền xung quanh

Lý do để layer nó theo cách này là tách biệt. Kiểu TPdf cốt lõi giữ nguyên như vậy, không có trường nào được thêm và không có chữ ký hiện có nào bị chạm, vì vậy một project không bao giờ cần bố cục không bao giờ mang code đo lường. Một project cần nó thêm một unit vào mệnh đề uses và các phương thức sáng lên. Khả năng trở thành opt-in ở mức độ chi tiết của một unit đơn, đó là cách sạch sẽ nhất để mở rộng một kiểu bạn không sở hữu hoặc không muốn làm phiền

uses
  PDFium, FPdfView, FPdfEdit,
  FPdfMeasure;   // the helper unit; brings MeasureText into scope on TPdf

// With the unit in scope the methods read as members of TPdf:
var
  W, H: Double;
begin
  Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
  // W and H are now the rendered width and height in PDF user units
end;

Đo lường mà không chạm đến trang

Đo lường phải không có tác dụng phụ. Nó phải báo cáo chiều rộng mà không để lại bất cứ thứ gì, vì bạn gọi nó nhiều lần trong khi quyết định bố cục và trang phải trông chính xác như thể bạn chưa bao giờ đo. Kỹ thuật làm điều này có thể là xây dựng một đối tượng văn bản, hỏi về kích thước của nó, và vứt bỏ nó trước khi nó bao giờ được gắn vào trang

Chuỗi là bốn lời gọi PDFium. FPDFPageObj_NewTextObj tạo một đối tượng văn bản đối với tài liệu, với tên font và kích thước. FPDFText_SetText đặt chuỗi mà đối tượng đó mang. FPDFPageObj_GetBounds đọc lại bounding box của đối tượng. FPDFPageObj_Destroy giải phóng đối tượng. Quan trọng là không có gì trong chuỗi đó gọi API chèn trang. Đối tượng được tạo, truy vấn và phá hủy trong sự cô lập, vì vậy tài liệu không thay đổi khi hàm trả về. Đó là một probe dùng xong mà đầu ra duy nhất của nó là bốn số của bounding box

Đây là cách mạnh mẽ để làm điều đó vì PDFium không expose chiều rộng tiến của từng glyph mà bạn có thể tự tổng hợp. Các chỉ số glyph phụ thuộc vào chương trình font, vào encoding, và vào cách PDFium tải face, và không có lời gọi công khai nào trao cho bạn advance của từng ký tự trong một chuỗi. Bounding box của một đối tượng văn bản thực, mặt khác, được tính bởi cùng máy móc sẽ bố trí các glyph để vẽ, vì vậy nó phản ánh phạm vi kết xuất thực tế thay vì xấp xỉ. Xây dựng một đối tượng dùng một lần và đọc bounds của nó là phép đo đáng tin cậy nhất mà thư viện có thể cung cấp

// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
  FontSize: Single; out Width, Height: Double);
var
  TextObject: FPDF_PAGEOBJECT;
  L, B, R, T: Single;
begin
  Width  := 0;
  Height := 0;
  if Self.Document = nil then
    Exit;
  TextObject := FPDFPageObj_NewTextObj(Self.Document,
    FPDF_BYTESTRING(AnsiString(Font)), FontSize);
  if TextObject = nil then
    Exit;
  try
    if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
      Exit;
    if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
    begin
      Width  := R - L;
      Height := T - B;
    end;
  finally
    FPDFPageObj_Destroy(TextObject);   // probe discarded, page untouched
  end;
end;

Tọa độ và đơn vị của kết quả

Bounding box trả về dưới dạng bốn cạnh, trái, dưới, phải và trên, và hai chiều xuất hiện bằng phép trừ. Chiều rộng là phải trừ trái và chiều cao là trên trừ dưới. Cả hai được biểu thị theo đơn vị người dùng PDF, trong đó một đơn vị là một phần bảy mươi hai inch, cùng không gian tọa độ mà trong đó bạn định vị văn bản trên trang. Không có đơn vị thiết bị ẩn và không có pixel liên quan ở giai đoạn này. Chiều rộng 36 có nghĩa là nửa inch của trang, bất kể độ phân giải kết xuất cuối cùng

Trục dọc chạy theo cách PDF định nghĩa nó, với Y tăng lên trên, đó là lý do tại sao chiều cao là trên trừ dưới chứ không phải ngược lại. Chi tiết đó quan trọng khi bạn tiến con trỏ xuống một cột. Bạn đo chiều cao của một dòng, rồi trừ nó khỏi baseline hiện tại để tìm dòng tiếp theo, vì di chuyển xuống trang có nghĩa là di chuyển về phía Y nhỏ hơn. Nếu đích đến của bạn là màn hình thay vì giấy, bạn chuyển đổi đơn vị người dùng sang pixel thiết bị với độ phân giải hiển thị: một giá trị theo đơn vị người dùng nhân với DPI và chia cho 72 cho pixel, vì vậy chiều rộng cột bạn đặt theo points có thể được khớp với một đoạn được đo trước khi bạn quyết định điểm ngắt đi đâu

Điều gì xảy ra với đầu vào degenerate

Các hàm được viết để thất bại yên lặng. Nếu không có tài liệu nào đang mở, hoặc nếu đối tượng văn bản không thể được tạo, kết quả là phạm vi không thay vì một exception được raise. Chiều rộng và chiều cao được khởi tạo về không ở đầu và chỉ được ghi đè một khi bounding box đã được đọc lại thành công. Chuỗi rỗng, tài liệu thiếu, font mà thư viện không thể giải thành đối tượng, mỗi cái trong số này trả về không thay vì ném

Lựa chọn đó giữ một vòng lặp đo lường đơn giản, vì một vòng lặp chạy qua hàng nghìn từ không phải là nơi xử lý exception trên mỗi iteration. Chi phí là người gọi mang kiểm tra. Chiều rộng không là một sentinel, không phải là sự thật về văn bản, vì vậy code chia cho chiều rộng được đo hoặc giả định giá trị dương phải bảo vệ chống lại không trước khi tin tưởng nó. Coi không là "không thể đo" và hợp đồng rõ ràng; bỏ qua nó và một đầu vào degenerate âm thầm trở thành bố cục với một cột các glyph chồng chéo

Xuống dòng tham lam được xây dựng trên phép đo

Với hàm chiều rộng trong tay, xuống dòng là một vòng lặp tham lam ngắn. Bạn chia đoạn văn thành các từ, giữ một dòng hiện tại, và cho mỗi từ bạn đo dòng sẽ ra sao nếu bạn thêm từ đó. Miễn là dòng thử nghiệm vẫn vừa chiều rộng cột bạn tiếp tục thêm; khi nó sẽ tràn bạn xả dòng hiện tại với AddText và bắt đầu một dòng mới với từ không vừa. Sự tích lũy được thực hiện hoàn toàn với MeasureTextWidth, và điều duy nhất đến trang là một dòng bạn đã xác nhận là vừa

procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
  FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
  Words: TArray<WideString>;
  Line, Trial: WideString;
  I: Integer;
  Y: Double;
begin
  Words := WideString(Para).Split([' ']);
  Line  := '';
  Y     := TopY;
  for I := 0 to High(Words) do
  begin
    if Line = '' then
      Trial := Words[I]
    else
      Trial := Line + ' ' + Words[I];
    // Measure the candidate line before drawing anything.
    if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
    begin
      Pdf.AddText(X, Y, Font, FontSize, Line);   // flush the line that fit
      Y    := Y - LineHeight;                    // Y decreases going down
      Line := Words[I];                          // overflowing word starts next line
    end
    else
      Line := Trial;
  end;
  if Line <> '' then
    Pdf.AddText(X, Y, Font, FontSize, Line);      // flush the final line
end;

Vòng lặp đo dòng thử nghiệm thay vì đo từng từ và tổng hợp, vì chiều rộng của một dòng không phải là tổng chiều rộng các từ của nó. Khoảng cách giữa các từ đóng góp, và một đoạn được đo nắm bắt điều đó trực tiếp. Quy tắc tham lam, vừa nhiều từ nhất có thể với chiều rộng cột cho phép và ngắt tại từ cuối cùng vừa, là cùng quy tắc lấp đầy khoảng cách giữa một AddText thô và một đoạn văn thực. Lời gọi vẽ chưa bao giờ là phần khó. Phép đo phải đi trước nó mới là, và đó chính xác là những gì helper cung cấp

Vị trí của tính năng này

Đo lường là lớp giữa tạo nội dung và kết xuất nó, vì vậy nó kết hợp tự nhiên với phần còn lại của quy trình tài liệu từ đầu. Nếu bạn đang lắp ráp các trang và đặt văn bản ngay từ đầu, nền tảng nằm trong tạo tài liệu PDF từ đầu với PDFium component trong Delphi, nơi AddText và thiết lập trang được đề cập đầy đủ. Khi font bạn đang đo quan trọng không kém chuỗi, vì các chỉ số phụ thuộc vào face, phân tích thuộc tính font PDF với PDFium component trong Delphi cho thấy cách thư viện báo cáo thông tin font điều khiển các bounding box đó. Cả hai xây dựng trên cùng một binding, PDFium Component cho Delphi và Lazarus, nơi measurement helper đi kèm cùng các API tài liệu, trang và văn bản được mô tả trên blog này