Bài viết kỹ thuật

PDFlibPas: Gạch nối và cột văn bản cân bằng trong Delphi

PDFlibPas dàn một đoạn văn bản đã lưu giữ vào một đến 64 cột có chiều rộng bằng nhau bằng DrawTextFlowColumns, và ngắt dòng bằng cơ chế gạch nối theo ngôn ngữ có giới hạn ngay khi bạn gọi SetTextFlowLanguageSetTextFlowHyphenation. Chín ngôn ngữ được hỗ trợ, và ngôn ngữ có thể được kế thừa từ giá trị /Lang trong Catalog của tài liệu thay vì phải đặt riêng cho từng luồng

Cả hai tính năng đều tồn tại vì cùng một lý do: một cột hẹp là nơi kiểu ngắt dòng ngây thơ ngừng trông giống dàn trang chuyên nghiệp và bắt đầu trông giống một báo cáo lỗi

Vì sao văn bản canh đều lại vỡ vụn trong các cột hẹp?

Vì việc canh đều phân bổ khoảng trống dư ra vào các khoảng cách giữa từ trên một dòng, và lượng dư đó phụ thuộc vào những gì vừa khít. Ở một khổ rộng, phần dư nhỏ và mắt người đọc không nhận ra. Giảm chiều rộng đi một nửa, một từ dài duy nhất không vừa sẽ bị đẩy xuống dòng tiếp theo, khiến các từ trước nó phải hấp thụ toàn bộ khoảng trống đó. Ba dòng như vậy liên tiếp tạo ra các dải trắng dọc mà giới typography gọi là "sông" (rivers), và người đọc cảm nhận đó là văn bản khó theo dõi mà không hiểu vì sao

Gạch nối khắc phục nguyên nhân thay vì triệu chứng, bằng cách cho phép ngắt ngay bên trong từ. Các từ ghép tiếng Đức và tiếng Hà Lan khiến điều này trở thành bắt buộc: một danh từ dài 24 ký tự trong một cột rộng 60 mm không có kết quả nào ổn nếu thiếu điểm ngắt. Tiếng Anh chịu đựng việc thiếu gạch nối tốt hơn, đó là lý do các sản phẩm ưu tiên tiếng Anh thường xuất xưởng mã dàn trang gãy đổ ngay lần đầu tiên một khách hàng Đức chạy thử

Những ngôn ngữ nào, và ngôn ngữ được lấy từ đâu?

Gạch nối hỗ trợ tiếng Anh, tiếng Đức, tiếng Hà Lan, tiếng Pháp, tiếng Tây Ban Nha, tiếng Ý, tiếng Bồ Đào Nha, tiếng Nga và tiếng Thổ Nhĩ Kỳ. Đặt ngôn ngữ trực tiếp cho từng luồng bằng SetTextFlowLanguage, hoặc để nó được kế thừa từ mục /Lang trong Catalog của tài liệu, chính là giá trị mà một tài liệu đã gắn thẻ và có khả năng truy cập đã mang sẵn

Việc kế thừa đó đáng để tận dụng hơn là ghi đè. Một tài liệu khai báo ngôn ngữ của nó trong Catalog đang nói cùng một sự thật cho trình đọc màn hình, bộ chỉ mục tìm kiếm và cơ chế gạch nối từ cùng một nơi, và một nơi duy nhất chính là nơi một sự thật nên tồn tại. Nếu bạn đã tạo đầu ra có gắn thẻ như mô tả trong gắn thẻ tự động cho PDF có khả năng truy cập, mục ngôn ngữ đã được đặt sẵn và luồng văn bản chỉ cần theo đó mà dùng

var
  Lib: TPDFlib;
  Flow, Drawn: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('Georgia', 1);
    Lib.SetTextSize(10.5);

    Flow := Lib.NewTextFlow(ArticleBody);
    try
      Lib.SetTextFlowLanguage(Flow, 'de');
      // Bật, tối thiểu 3 ký tự trước điểm ngắt, 3 ký tự sau
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // không bao giờ để lẻ một dòng

      repeat
        // Ba cột trên một vùng 480 pt, rãnh 18 pt, cân bằng
        Drawn := Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);
        if (Drawn = 0) or (Lib.TextFlowFinished(Flow) = 1) then
          Break;
        Lib.NewPage;
      until False;
    finally
      Lib.ReleaseTextFlow(Flow);
    end;

    Lib.SaveToFile('newsletter.pdf');
  finally
    Lib.Free;
  end;
end;

MinPrefix và MinSuffix là vấn đề typography, không phải kiểm tra hợp lệ

Hai số nguyên sau cờ bật quy định số ký tự tối thiểu phải còn lại trước và sau một điểm ngắt. Ba và ba là giá trị mặc định thận trọng mà hầu hết các quy chuẩn trình bày chấp nhận. Hai và hai tạo ra nhiều cơ hội ngắt hơn và kết quả xấu đi rõ rệt, vì một mẩu hai chữ cái lơ lửng ở cuối dòng đọc lên như một lỗi đánh máy

Hãy tăng các giá trị tối thiểu khi cỡ chữ lớn, nơi mỗi mẩu ký tự đều nổi bật về mặt thị giác, và chỉ giảm chúng khi cột thực sự hẹp và bạn đã quyết định rằng khổ chữ chật hẹp quan trọng hơn một kết quả sạch sẽ. Đây là quyết định về quy chuẩn trình bày hơn là quyết định kỹ thuật, và chính vì thế nó là một tham số chứ không phải một hằng số

"Cân bằng" ở đây thực sự có nghĩa là gì?

Tham số Balance chỉ thay đổi hành vi ở cuối một đoạn văn bản. Khi bật cân bằng, các cột được rút ngắn về đúng một số dòng bằng nhau khi toàn bộ phần còn lại vừa vặn trong vùng đó, đó chính là điều ngăn trang cuối cùng hiển thị hai cột đầy và một cột thứ ba chỉ có mỗi một dòng cô đơn. Khi đoạn văn bản không vừa hết, mỗi cột vẫn giữ nguyên chiều cao đầy đủ để trang chứa được nhiều văn bản nhất có thể, và phần còn lại tiếp tục sang trang sau

Sự bất đối xứng đó là mặc định đúng đắn cho các tài liệu liên tục. Cân bằng ở giữa một bài viết đang chảy liên tục sẽ lãng phí không gian dọc trên mọi trang cho một hiệu ứng thẩm mỹ mà không ai nhìn thấy, vì các cột vốn đã đầy sẵn. Cân bằng ở cuối mới là nơi mắt người thực sự nhận ra, và đó chính xác là nơi nó được áp dụng

Ngắt dòng đo theo cả từ trọn vẹn

Thuật toán ngắt dòng đo theo các từ trọn vẹn thay vì cộng dồn chiều rộng từng ký tự, và dành riêng một lượt tìm kiếm có giới hạn cho các token quá khổ không thể vừa một dòng nào cả, chẳng hạn một URL hay một số hiệu tài khoản. Điều đó giữ cho trường hợp phổ biến chạy nhanh và trường hợp bất thường vẫn có giới hạn, thay vì ngược lại

Dấu gạch nối tùy chọn và dấu gạch nối tự động chỉ được vẽ ra khi điểm ngắt mà chúng đánh dấu là điểm ngắt thực sự được chọn. Điều này nghe có vẻ hiển nhiên nhưng lại là một lỗi kinh điển: một cách triển khai ngây thơ sẽ ghi ký tự gạch nối ngay trong lúc đo, và nếu điểm ngắt dịch chuyển, dấu gạch nối vẫn ở lại giữa dòng. Không gì trông giống một bộ máy xử lý văn bản bị hỏng hơn một dấu gạch nối lạc lõng ngay giữa một từ

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // Quyết định bố cục trước khi vẽ bất cứ thứ gì
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // Số dòng phần văn bản còn lại cần ở một chiều rộng cột
    Needed := Lib.MeasureTextFlow(Flow, 148);
    if Needed > 3 * LinesPerColumn then
      UseTwoPageSpread
    else
      UseSinglePage;

    Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);

    if Lib.TextFlowFinished(Flow) <> 1 then
      CarryOver(Lib.GetTextFlowRemaining(Flow));
  finally
    Lib.ReleaseTextFlow(Flow);
  end;
end;

Giữ nguyên thiết lập phông trên mọi hộp

Một quy tắc chi phối mọi bố cục dựa trên luồng văn bản và đáng để nói thẳng: DrawTextFlow, DrawTextFlowColumnsMeasureTextFlow đều ngắt dòng dựa trên phông được chọn tại thời điểm chúng được gọi. Đổi phông hoặc cỡ chữ giữa hai hộp của cùng một luồng, hoặc sang trang mới mà không chọn lại phông, và hộp thứ hai sẽ ngắt dòng khác với những gì hộp đầu tiên đã đo

Triệu chứng này gây khó chịu chính vì nó trông có vẻ ngẫu nhiên: văn bản vừa khít trên trang một lại tràn ra ở trang hai, hoặc số dòng đã đo không khớp với những gì thực sự được vẽ. Hãy chọn phông một lần trước vòng lặp, chọn lại sau mỗi NewPage, và luồng văn bản sẽ hoạt động đúng. Khi nhiều hệ chữ viết xuất hiện trong cùng một đoạn văn bản, cách xử lý được mô tả trong dự phòng phông tự động cho CJK và emoji áp dụng cho cả việc đo lẫn việc vẽ, nên chiều rộng vẫn nhất quán qua cả các đoạn dự phòng

Đối với các bố cục báo cáo, nơi luồng văn bản chỉ là một phần tử bên cạnh đầu trang, chân trang và các khối dữ liệu, các mẫu bố cục trong bộ máy báo cáo tập dữ liệu kết hợp gọn gàng với các luồng cột: đo trước, đặt các phần cố định, rồi mới giao cho luồng văn bản bất kỳ vùng nào còn lại

PDFlibPas là thư viện PDF cho Delphi, C++Builder và Lazarus, và toàn bộ vòng đời của TextFlow, từ tạo, vẽ, đo, kiểm tra, tua lại đến giải phóng, cũng được cung cấp qua giao diện DLL và ActiveX. Tài liệu đầy đủ có tại trang thư viện PDF PDFlibPas cho Delphi