Bài viết kỹ thuật

Shaping văn bản cắm được: Uniscribe và HarfBuzz trong Delphi

Shaping văn bản trong thành phần PDFium đi qua một đối tượng cài được duy nhất. ConfigureTextShaper cài shaper mà mọi điểm vào shaping đều dẫn qua, thay thế và giải phóng thứ đang có ở đó; ActiveTextShaper trả về shaper đã cài và tạo mặc định nền tảng ở lần dùng đầu; ActiveTextShaperName báo backend nào đang sống; ClearTextShaper bỏ cài đặt và cho phép mặc định được tạo lại. Trên Windows, mặc định là TPdfUniscribeTextShaper. Dưới Free Pascal có TPdfHarfBuzzTextShaper, hàm gắn libharfbuzz lúc chạy để một thư viện còn thiếu là một tình trạng được báo cáo chứ không phải một thất bại nạp

Kiến trúc shaping văn bản cắm được trong thành phần Delphi PDFium: ConfigureTextShaper, ActiveTextShaper và ClearTextShaper quản lý một backend đã cài, Uniscribe trên Windows và HarfBuzz gắn lúc chạy dưới Free Pascal
Mọi lệnh gọi shaping đều dẫn qua đối tượng shaper đã cài duy nhất, với một mặc định nền tảng trên mỗi mục tiêu

Một giao diện, hai backend chia việc theo cách hoàn toàn khác nhau. Việc hiểu sự bất đối xứng đó là điều ngăn con đường di động tạo ra văn bản được shaping đúng nhưng định vị sai

Vì sao backend Windows là một lớp còn bản di động là ba mảnh?

Vì Uniscribe là bốn API giả làm một. ScriptItemize phân đoạn một chuỗi theo script và phân giải các mức hai chiều; ScriptShape ánh xạ ký tự thành glyph; ScriptPlace tính advance và offset; ScriptLayout xếp các run kết quả vào thứ tự thị giác. Một backend dựng trên nó vì vậy chẳng còn gì để thêm, đó là lý do shaper Windows là một lớp với một phương thức

HarfBuzz phủ hai cái giữa. Nó shaping và định vị một run mà caller đã quyết định sẵn hướng và script, và nó không có ý kiến gì về cách một đoạn văn tách thành các run hay các run đó xuất hiện theo thứ tự nào. Vì vậy backend di động cung cấp phần còn lại: thuật toán hai chiều phân giải các mức nhúng, các hàm Unicode HarfBuzz phân đoạn văn bản theo script, và các run được xếp theo thứ tự thị giác mà quy tắc L2 của UAX #9 tạo ra. Nửa hai chiều đồ sộ đủ để là một unit riêng, được mô tả trong bài viết về các mức nhúng UAX #9

So sánh pipeline shaping cho văn bản PDF: Uniscribe cung cấp ScriptItemize, ScriptShape, ScriptPlace và ScriptLayout trong một lớp, trong khi HarfBuzz chỉ phủ shaping và định vị quanh các tầng UAX #9 riêng của thành phần
Uniscribe phủ cả bốn tầng; con đường di động phải tự cung cấp itemization và thứ tự thị giác

Shaper không phân giải phông, và điều đó là có chủ đích

Uniscribe đọc nhị phân phông từ một device context GDI. Chẳng có đối tượng tương đương di động của điều đó, và việc bịa một cái bên trong một unit shaping nghĩa là quyết định, thay mặt mọi ứng dụng, rằng phông đến từ fontconfig, từ CoreText, từ một thư mục phông ứng dụng, hay từ một cơ sở dữ liệu. Vì vậy backend HarfBuzz nhận một resolver: một callback ánh xạ một tên phông tới các byte TrueType hay OpenType. Trả về False làm thất bại yêu cầu shaping giống như một phông GDI không đọc được làm thất bại nó trên Windows

uses
  FPdfTextShaping
{$IFDEF FPC}
  , FPdfTextShapingHb
{$ENDIF}
  ;

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Chính sách của bạn: fontconfig, CoreText, một thư mục phông
  // ứng dụng, một CSDL
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // Quyền sở hữu chuyển cho unit; gọi một lần lúc khởi động,
  // trước khi bất cứ thứ gì shaping văn bản
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // Trên Delphi, mặc định nền tảng (Uniscribe) được tạo theo yêu cầu,
  // nên chẳng cần cài đặt gì cả
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Giữ việc khám phá phông bên ngoài shaper có một lợi ích thứ hai lộ ra trên máy chủ: cùng một tiến trình có thể shaping với một tập phông nhúng chẳng liên quan gì tới thứ được cài trên máy, đúng thứ bạn muốn khi đầu ra phải tái tạo được từng byte giữa các máy chủ. Thành phần cũng phơi một provider phông hệ thống máy chủ cho các trường hợp bạn thực sự muốn phông đã cài, được đề cập trong bài viết về provider phông hệ thống

Record kết quả trung lập với backend, và cluster là lý do

Cả hai backend đều điền cùng một TPdfShapedText: văn bản nguồn, tên phông, cỡ, byte phông, một mảng run, tổng bề rộng, số glyph và số ký tự logic. Mỗi TPdfShapedRun mang đoạn của nó trong văn bản nguồn, vị trí X thị giác, bề rộng, mức hai chiều và một cờ phải-sang-trái, cộng với các glyph của nó. Mỗi TPdfShapedGlyph mang một định danh glyph, một advance, các offset X và Y, và cluster mà nó thuộc về dưới dạng một điểm bắt đầu và một độ dài trong văn bản nguồn

Những trường cluster đó là điều khiến bản ghi dùng được chứ không chỉ có tính thông báo. Shaping không phải một ánh xạ một-một: một âm tiết Devanagari thành một glyph từ bốn ký tự, một ligature Ả Rập gộp hai ký tự, và một ký tự đơn có thể tạo ra vài dấu phụ. Không có đoạn cluster, bạn không thể đặt một caret, hit-test một cú nhấp, hay tô sáng một vùng chọn, vì bạn không thể nói glyph thuộc về những ký tự nào. Có chúng, phép tính là cục bộ và cùng một mã chạy cho cả hai backend

Các đoạn cluster glyph trong TPdfShapedText: một glyph âm tiết Devanagari từ bốn ký tự, một ligature Ả Rập từ hai, và một chữ gốc cộng dấu phụ từ một ký tự, mỗi cái được ánh xạ ngược qua ClusterStart và ClusterLength
Các đoạn cluster ánh xạ mọi glyph ngược về các ký tự nguồn của nó để caret, hit test và vùng chọn hoạt động
var
  Shaped: TPdfShapedText;
  R, G: Integer;
begin
  if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
    for R := 0 to High(Shaped.Runs) do
    begin
      // Các run đã đến theo thứ tự thị giác với VisualX được điền sẵn
      X := Shaped.Runs[R].VisualX;
      for G := 0 to High(Shaped.Runs[R].Glyphs) do
      begin
        EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
          X + Shaped.Runs[R].Glyphs[G].OffsetX,
          Shaped.Runs[R].Glyphs[G].OffsetY);
        X := X + Shaped.Runs[R].Glyphs[G].Advance;
      end;
    end;
end;

Ngân sách thuộc về record tùy chọn

TPdfTextShapingOptions mang một hướng cộng ba trần: ký tự tối đa, glyph tối đa và run tối đa, với một hàm lớp Default điền các giá trị hợp lý. Các trần không phải hoang tưởng về đầu vào dị dạng; chúng là phép toán. Shaping mở rộng: một phông với phép thay thế ngữ cảnh hung hãn có thể phát nhiều glyph hơn ký tự đầu vào, và một đoạn văn so le script mỗi vài ký tự tạo ra một run cho mỗi lần chuyển. Một tài liệu được lắp ráp để tối đa hóa cả hai biến một chuỗi khiêm tốn thành một lần cấp phát lớn, và một dịch vụ shaping văn bản từ các PDF không tin cậy cần một giới hạn mà nó chọn chứ không phải một giới hạn mà máy áp đặt

Đặt hướng tường minh thay vì để chế độ tự động đáng làm bất cứ khi nào bạn đã biết nó. Chế độ tự động áp các quy tắc hướng đoạn để đoán từ ký tự mạnh đầu tiên, điều đúng cho văn bản tự do và sai cho một trường biểu mẫu mà hướng là thuộc tính của trường chứ không phải của giá trị ai đó gõ vào

Gắn lúc chạy, không phải phụ thuộc build

Backend HarfBuzz nạp thư viện một cách động. Đó là một quyết định triển khai với hậu quả thật: một binary chạy trên máy có HarfBuzz và trên máy không có, báo năng lực giảm trong trường hợp thứ hai thay vì không khởi động nổi. Với một thư viện phát hành cho các lập trình viên khác, đó là cách sắp xếp duy nhất khả thi, vì bạn không thể đòi hỏi mọi người tiêu dùng của một thành phần PDF phải sắm và khớp phiên bản một thư viện shaping mà họ có thể không cần

Quy tắc tương ứng cho caller là kiểm tra. ActiveTextShaper trả về nil khi nền tảng không có mặc định và chẳng cái nào được cấu hình, và điểm vào shaping báo điều đó như một shaper không khả dụng chứ không phải một thất bại shaping. Đó là hai vấn đề khác nhau và đáng nhận các thông điệp khác nhau: một là khoảng trống triển khai, kia là vấn đề phông hay văn bản

Cài một lần, trước khi bất cứ thứ gì shaping

Việc cài đặt thay thế và giải phóng shaper trước đó, nên gọi nó lặp đi lặp lại là an toàn nhưng vô nghĩa, còn gọi nó trong khi một thread khác đang shaping thì chẳng an toàn chút nào. Hãy làm lúc khởi động. Nếu bạn cần quay về mặc định nền tảng sau đó, hãy truyền nil, cũng là cách bạn gỡ một test double ở cuối một phép thử

Một khi backend đã được cài, đo lường và ngắt dòng hành xử như nhau trên cả hai nền tảng, vì chúng tiêu thụ các số đo run và glyph thay vì gọi nền tảng trực tiếp; mô hình ngắt dòng được mô tả trong bài viết về đo văn bản và ngắt từ. Các nền tảng và toolchain được hỗ trợ của thành phần được liệt kê trên trang sản phẩm PDFium Delphi component