Bài viết kỹ thuật

Trích văn bản PDF trong Delphi: khoảng cách từ và xuống dòng

HotPDF Delphi Component dựng lại khoảng cách từ và xuống dòng trong THotPDF.ExtractLoadedPageText từ hình học của glyph, chứ không phải từ các ký tự dấu cách. Một dấu cách được chèn vào khi khoảng hở sau bề rộng riêng của glyph vượt quá 0.15 bề cao văn bản, còn một dòng mới chỉ bắt đầu khi gốc văn bản trôi qua hướng viết quá nửa bề cao văn bản. Kể từ v2.768.3, văn bản trang còn bao gồm cả văn bản được vẽ qua Form XObject và loại bỏ các glyph nằm ngoài vùng crop nhìn thấy được. Phần còn lại của bài giải thích vì sao mỗi luật lại có dạng như vậy, vì từng luật đều đã thay thế một luật đơn giản hơn từng cho ra output nghe rất hợp lý nhưng sai trên các tài liệu ngoài đời

Các triệu chứng thì quen thuộc với bất kỳ ai từng đổ văn bản PDF vào một bộ chỉ mục tìm kiếm. Một trang bìa trích ra thành PDFReferenceManualNovember4,1998, một mẫu tờ khai thuế tách thành 156 dòng, một watermark chéo về từng dòng một ký tự, còn một bản proof đã trim lại mở đầu bằng dòng slug của máy in mà chẳng viewer nào từng hiển thị. Chẳng tệp nào trong số đó bị hỏng. Mỗi tệp dùng một cách đặt văn bản hoàn toàn hợp lệ mà một bộ trích ngây thơ đọc nhầm

Vì sao văn bản PDF trích ra mất dấu cách giữa từ?

Văn bản trích ra mất dấu cách vì một PDF không bao giờ bị bắt buộc phải chứa chúng. Producer có thể tách các từ bằng cách hiển thị ký tự dấu cách, nhưng cũng có thể kéo bút đi bằng một con số trong mảng TJ (ISO 32000-1 §9.4.3) hay bằng một Td mới (§9.4.2), và output TeX, nhiều tệp Distiller cùng phần lớn bố cục canh đều hai bên làm đúng như vậy. Trước v2.766.76, HPDFAssemblePageText chỉ nhìn chuyển động thẳng đứng, nên một lần ngắt từ sinh ra từ định vị đơn giản là bốc hơi. Bộ ghép giờ đo, dọc theo hướng viết của glyph trước, khoảng cách từ điểm cuối bề rộng riêng của glyph đó tới gốc của glyph hiện tại, và chèn một dấu cách khi khoảng cách vượt quá 0.15 bề cao hộp của glyph hiện tại, tính từ ascent tới descent trong user space. Chẳng có dấu cách nào được thêm khi một trong hai bên vốn đã trống, và chẳng có gì giữa hai ký tự CJK, vì canh đều giãn các chữ tượng hình ra xa mà sự giãn ấy chẳng có nghĩa là một biên giới từ. Các record glyph lộ đúng hình học đó, nên bạn có thể dựng lại quyết định khi một tệp cụ thể khiến bạn bối rối

uses
  SysUtils, HPDFDoc, HPDFContentStream;

procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
  Glyphs: THPDFGlyphArray;
  I: Integer;
  Height, Gap: Double;
begin
  if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
    Exit;
  for I := 1 to High(Glyphs) do
  begin
    // bề cao ascent-tới-descent của hộp glyph, trong user space
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // văn bản ngang: khoảng hở tính từ điểm cuối bề rộng của glyph trước
    Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
    if (Height > 0) and (Gap > 0.15 * Height) then
      Writeln(Format('U+%.4x gap %.2f height %.2f: space',
        [Glyphs[I].Unicode, Gap, Height]));
  end;
end;

Vì sao đo từ bề rộng riêng của glyph thay vì vị trí bút?

HotPDF đo khoảng cách từ từ GlyphEndX / GlyphEndY vì vị trí bút sau một glyph đã chứa sẵn phần giãn cách không phải là khoảng hở. ISO 32000-1 §9.4.4 định nghĩa độ dịch ngang là bề rộng glyph nhân cỡ chữ, cộng character spacing Tc, cộng word spacing Tw, tất cả nhân với Tz. BaselineEndX / BaselineEndY giữ trọn phần dịch ấy, trong khi GlyphEndX / GlyphEndY chỉ giữ font advance và Tz. Sự khác biệt này quan trọng với các producer siết tracking bằng Tc âm rồi hoàn lại dấu cách qua một bước chỉnh TJ sau mỗi glyph: đo từ vị trí bút, phần hoàn lại trông như một khoảng hở, và thuật ngữ tiếng Trung “95后” bị trích thành “9 5 后”. Ngưỡng bị gắn vào bề cao hộp glyph chứ không phải cỡ Tf cũng vì lý do tương tự. Word xuất file hay ghi 1 Tf và mang cỡ thật trong một Tm đã scale, nên Tfs nói 1 trong khi văn bản cao 10 điểm, và một luật dựa trên Tfs sẽ đối xử khác nhau với hai cách viết của cùng một trang

Luật khoảng cách từ của HotPDF cho ExtractLoadedPageText trong Delphi: một dấu cách chỉ được chèn khi khoảng cách từ GlyphEndX của glyph trước tới BaselineStartX của glyph kế vượt quá 0.15 bề cao hộp ascent-tới-descent, vì vị trí bút trong BaselineEndX đã chứa Tc, Tw và Tz và biến các phần hoàn lại tracking canh đều thành khoảng hở giả kiểu 9 5 后
Hình học, chứ không phải ký tự dấu cách, quyết định từ ngắt ở đâu — các record glyph lộ đúng cùng số đo đó, nên bạn có thể dựng lại quyết định cho bất kỳ tệp nào gây bối rối

Luật này có những cạnh thẳng thắn. Một tiêu đề đặt với tracking rất lỏng, nơi một mình Tc mở quá 0.15 bề cao văn bản giữa các chữ, sẽ trích ra với một dấu cách giữa từng chữ, đúng như trang trông thấy nhưng có khi chẳng phải thứ bạn muốn đưa vào chỉ mục. Những mảnh được vẽ trái thứ tự trên cùng một baseline cho ra khoảng cách âm và dính vào nhau không dấu cách. Chẳng ca nào phổ biến trong phần thân văn bản, và trên một kho test, thay đổi này nâng số từ khớp so với một bộ trích tham chiếu trên 28 trang mà không hạ trang nào

HotPDF xuống dòng mới trong văn bản trích ra khi nào?

Kể từ v2.766.79, một dòng mới bắt đầu khi bước dịch từ gốc của glyph trước tới glyph hiện tại, chiếu lên pháp tuyến của hướng viết trước đó, vượt quá nửa bề cao hộp lớn hơn trong hai glyph. Luật cũ so dịch chuyển Y thô với một nửa Tfs, và hỏng theo hai hướng. Với 1 Tf và một Tm đã scale, ngưỡng co xuống nửa đơn vị, nên một superscript nhô lên bằng text rise 0.4 hay chính độ rung baseline tầm thường cũng phá vỡ dòng. Luật cũ còn bỏ quên X hoàn toàn, nên văn bản dưới một Tm bị xoay trôi dần xuống trang theo từng glyph và ra thành mỗi dòng một glyph. Chiếu lên pháp tuyến của hướng khiến các run xoay hành xử như run ngang, còn việc lấy bề cao lớn hơn trong hai cái giữ một từ mẫu to đùng với chú thích nhỏ của nó trên cùng một dòng khi chúng chung baseline. Ở tờ khai thuế nhắc ở trên, số dòng tụt từ 156 xuống 97. Văn bản dọc ở writing mode 1 (§9.7.4.3) đi một đường riêng: các glyph đó được nhóm thành cột, đọc từ phải qua trái và từ trên xuống dưới, xuống dòng tại mỗi lần đổi cột

ExtractLoadedPageText của HotPDF quyết định xuống dòng trong Delphi ra sao: bước dịch giữa các gốc glyph được chiếu lên pháp tuyến của hướng viết rồi so với nửa bề cao hộp lớn hơn, nên một superscript nhô lên bằng text rise nhỏ dưới font 1 Tf và văn bản trôi xuống trang dưới một Tm xoay không còn bị tách thành mỗi dòng một glyph
Phép chiếu khiến các run xoay hành xử như run ngang, còn lấy bề cao hộp lớn hơn trong hai cái giữ một từ mẫu to với chú thích nhỏ của nó trên cùng một dòng

ExtractLoadedPageText bao gồm và loại bỏ những văn bản nào?

ExtractLoadedPageText trả về văn bản mà viewer hiển thị. Kể từ v2.766.80 nó làm việc chỉ từ các glyph nhìn thấy được, bỏ mọi glyph mà tâm hộp của nó rơi ngoài GetLoadedPageVisibleBox, tức CropBox bị cắt bởi MediaBox (§14.11.2). Điều đó dọn các dòng slug và các dấu in khác của máy in được đặt dưới dạng văn bản ngoài vùng trim. ExtractLoadedPageGlyphs được cố tình giữ cho trả về mọi glyph của content stream trang, nên bạn vẫn tìm được mấy thứ ấy khi cần. Bộ lọc là một phép thử hộp, không phải phép thử hiển thị: văn bản bị che bởi clipping path, được tô trắng hay bị ảnh đè lên vẫn được trích ra

var
  Pdf: THotPDF;
  Glyphs: THPDFGlyphArray;
  PageText: UnicodeString;
  L, B, R, T: Single;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('trimmed-proof.pdf');
    if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
      Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
    // mọi glyph của content stream trang, kể cả dòng slug
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // chỉ những gì trang hiển thị, với văn bản Form XObject được ghép vào
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

Văn bản được vẽ qua Form XObject nằm trong văn bản trang kể từ v2.768.3. Header, con dấu và watermark rất thường sống trong các form, và vài tài liệu tiêu chuẩn từng mất 30 tới 35 phần trăm ký tự trước thay đổi này. THotPDF.InterpretContentWithForms ghi lại từng Do cùng CTM đang có hiệu lực, giải thích form tại /Matrix của nó nhân với CTM ấy (§8.10.1), và ghép các glyph của form vào đúng vị trí của Do, đệ quy vào cả các form lồng nhau. Một form không có /Resources riêng sẽ mượn của stream vẽ ra nó, đúng như §7.8.3 cho phép. Glyph form mang TokenIndex = -1, và ExtractLoadedPageGlyphs vẫn chỉ trả về glyph của stream trang, vì tìm-kiếm, thay-thế và redaction ghi thay đổi ngược lại qua TokenIndex và sẽ sửa nhầm byte nếu một glyph form chui vào. Hai sự đơn giản hóa đáng biết: văn bản form không bị cắt theo /BBox của form, và đệ quy dừng ở 12 tầng thay vì dò chu trình, nên một form dị dạng tự vẽ chính nó sẽ lặp văn bản của nó cho tới khi chạm trần

HotPDF giữ những glyph nào khi trích văn bản trang PDF trong Delphi: ExtractLoadedPageText chỉ giữ glyph có tâm hộp nằm trong GetLoadedPageVisibleBox, tức CropBox cắt bởi MediaBox, nên các dòng slug của máy in biến mất, trong khi InterpretContentWithForms ghép glyph Form XObject tại từng vị trí Do với TokenIndex đặt bằng -1 và API mức glyph vẫn trả về tất cả
Một phép thử hộp trên tâm glyph không phải phép thử hiển thị — văn bản trắng, văn bản bị cắt và văn bản bị đè vẫn ra ngoài, còn văn bản form được tính kể từ v2.768.3

Vì sao văn bản sau toán tử Q lại giải mã thành rác?

Văn bản sau Q có thể bị giải mã sai trước v2.766.73 vì bộ trích chỉ lưu CTM khi gặp q. Các tham số trạng thái văn bản, tức font, cỡ, Tc, Tw, Tz, TL, chế độ render và độ nhô, thuộc về graphics state (§9.3.1), nên Q phải khôi phục chúng cùng mọi thứ trên stack (§8.4.2). Một báo cáo trong ngành từng chọn một font Identity-H hai byte bên trong q … Q rồi hiển thị văn bản WinAnsi một byte mà chẳng có Tf riêng. Bộ trích giữ nguyên font bên trong, đọc các dấu dẫn đầu và từ “Adobe” trong mục lục thành các mã hai byte, và làm rơi 15% ký tự của trang. Stack q/Q của interpreter giờ giữ trọn trạng thái văn bản. Các luật trích ở đây áp dụng cho mọi trang, nên cả một tài liệu có thể đổ ra tệp trong một lệnh gọi

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // phạm vi rỗng = mọi trang; form feed giữa các trang; BOM UTF-8
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

Nên dùng API văn bản HotPDF nào?

ExtractLoadedPageText giữ nguyên thứ tự content-stream, là mặc định đúng cho tìm kiếm và đánh chỉ mục; chuỗi giải mã bên dưới nó được bàn trong trích văn bản từ PDF đã nạp với HotPDF. Với các tài liệu có cấu trúc mà thứ tự soạn thảo quan trọng, trích văn bản theo thứ tự cấu trúc đi theo cây cấu trúc thay vì đoán từ hình học, còn với dữ liệu khóa trong bảng, trích bảng có kiểu xuyên qua các ngắt trang trả về từng ô thay vì từng dòng. Tài liệu API đầy đủ cùng bản dùng thử nằm trên trang sản phẩm HotPDF Delphi PDF Component