Bài viết kỹ thuật

Thêm Lớp Văn Bản Có Thể Tìm Kiếm vào PDF Scan trong Delphi

PDFium Component thêm một lớp văn bản có thể tìm kiếm vào các trang PDF scan từ Delphi thông qua ApplyOcrSearchLayer. Nó render mỗi trang được chọn, giao các điểm ảnh cho một bộ cung cấp OCR mà bạn tự cung cấp, rồi ghi các từ đã nhận diện được trở lại dưới dạng các đối tượng văn bản vô hình đặt chồng lên các từ trong bản scan. Hình ảnh trang gốc không bao giờ bị giải mã, mã hóa lại hay thay thế, nên kết quả trực quan giống hệt từng byte với trang bạn bắt đầu

Bộ máy nhận diện cố tình không phải là một phần của thư viện. PDFium cung cấp render trang, ánh xạ tọa độ, tải font, tạo đối tượng văn bản và các chế độ render vô hình, nhưng nó không chứa bộ máy OCR nào, và giả vờ ngược lại nghĩa là đóng gói sản phẩm nhận diện của người khác vào một thành phần PDF. Thay vào đó, việc nhận diện nằm sau giao diện IPdfOcrProvider: thư viện truyền các điểm ảnh BGRA có bố cục cố định, gốc trên cùng, và bộ cung cấp trả về văn bản Unicode, giá trị độ tin cậy và tứ giác của từng từ

Lớp văn bản có thể tìm kiếm thực chất là gì?

Một PDF scan là hình ảnh của một tài liệu. Nội dung trang là một hình ảnh lớn duy nhất, và không có gì để chọn, tìm kiếm, sao chép hay lập chỉ mục. Một lớp văn bản có thể tìm kiếm thêm các đối tượng văn bản thật lên trên hình ảnh đó với chế độ render đặt là vô hình, nên trình xem không vẽ ra gì ngoại trừ việc chọn văn bản, tìm kiếm và trích xuất tìm thấy các từ đúng ngay tại vị trí chúng xuất hiện

Định vị chính là toàn bộ vấn đề. Nếu văn bản vô hình lệch vài điểm, phần tô sáng khi chọn sẽ nằm cạnh các từ thay vì đè lên chúng, và sao chép một đoạn văn sẽ cho ra văn bản sai thứ tự. Đây là lý do vì sao hình học phải đến từ chính các phép biến đổi mà PDFium dùng để render trang thay vì từ một phỏng đoán theo tỷ lệ

Triển khai bộ cung cấp

Hợp đồng của bộ cung cấp chỉ gồm một phương thức. Nó nhận một bản ghi hình ảnh trang mang kích thước, stride, DPI, định dạng điểm ảnh và chính các byte điểm ảnh, cộng thêm một token hủy bỏ, và trả về các từ hoặc một thông báo lỗi:

uses
  PDFium;

type
  TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
  public
    function RecognizePage(const Image: TPdfOcrImage;
      const CancellationToken: IPdfCancellationToken;
      out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
  end;

function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
  const CancellationToken: IPdfCancellationToken;
  out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
  I: Integer;
begin
  // Image.Pixels chứa các hàng BGRA gốc trên, mỗi hàng dài Image.Stride byte.
  // Đưa chúng cho bộ máy của bạn, rồi điền một mục cho mỗi từ đã nhận diện
  SetLength(Words, RecognisedCount);
  for I := 0 to RecognisedCount - 1 do
  begin
    Words[I].Text := EngineWordText(I);
    Words[I].Confidence := EngineWordConfidence(I);   // 0..1
    Words[I].Quad := TPdfOcrQuad.FromRectangle(
      EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
  end;
  ErrorMessage := '';
  Result := True;
end;

Dùng tứ giác thay vì hình chữ nhật, vì một bản scan hiếm khi vuông vắn với trang. Một từ trên một trang hơi bị xoay lệch chiếm một hình bình hành, và TPdfOcrQuad mang bốn điểm góc để các từ bị nghiêng và xoay vẫn giữ được vùng chọn chính xác. Các bộ máy chỉ báo cáo hộp thẳng trục có thể dùng FromRectangle, hàm này dựng tứ giác suy biến

Vì sao vị trí từ không thể chỉ đơn giản co giãn theo tỷ lệ?

Thật hấp dẫn khi chuyển đổi một tọa độ điểm ảnh sang tọa độ trang bằng cách chia cho độ rộng render rồi nhân với độ rộng trang. Cách đó chỉ đúng với những trang không xoay, có CropBox giống hệt MediaBox, và gốc tọa độ tại điểm không, trong khi rất nhiều tài liệu scan không thỏa ít nhất một trong các điều kiện đó

PDFium Component ánh xạ từng góc trong bốn góc của tứ giác riêng lẻ thông qua FPDF_DeviceToPage, cùng phép ánh xạ mà renderer đã dùng để tạo ra các điểm ảnh, nên các mục /Rotate và crop box bị dịch chuyển đều được xử lý ngay từ cấu trúc. Ma trận affine cho đối tượng văn bản sau đó được dựng từ ba trong số các điểm đã ánh xạ, góc dưới-trái, dưới-phải và trên-trái, đủ để biểu diễn vị trí, tỷ lệ, xoay và trượt nghiêng

Bản thân đối tượng văn bản được tạo ở cỡ font đơn vị để có thể đo được biên font thực của nó, và biên đối tượng đã đo sau đó được ánh xạ lên tứ giác mục tiêu. Định cỡ theo một cỡ điểm phỏng đoán rồi hy vọng nó khớp với từ đã scan sẽ trôi dạt theo mỗi lần thay font; đo trước giúp việc khớp độc lập với font nào được dùng cho lớp này

Chạy nó trên toàn tài liệu

Bản ghi tùy chọn kiểm soát độ phân giải, bộ lọc và mọi ngân sách. Lọc theo độ tin cậy quan trọng hơn vẻ ngoài của nó: các từ rác ở độ tin cậy thấp làm ô nhiễm kết quả tìm kiếm vĩnh viễn, và không giống một lần render sai, không ai nhận ra cho đến khi một lượt tìm kiếm trả về những thứ vô nghĩa:

var
  Pdf: TPdf;
  Options: TPdfOcrOptions;
  Report: TPdfOcrReport;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'scanned-contract.pdf';
    Pdf.LoadDocument;

    Options := TPdfOcrOptions.Default;
    Options.Dpi := 300;                  // độ phân giải nhận diện
    Options.MinConfidence := 0.60;       // loại bỏ từ không chắc chắn
    Options.SkipPagesWithText := True;   // để yên các trang PDF sinh ra đã là văn bản
    Options.ContinueOnError := True;     // một trang lỗi không được làm dừng cả tác vụ
    Options.MaxPixelsPerPage := 40 * 1000 * 1000;

    if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
      Pdf.SaveAs('scanned-contract-searchable.pdf');

    for I := 0 to High(Report.Pages) do
      if Report.Pages[I].Status = popsFailed then
        Writeln(Format('page %d failed: %s',
          [Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
    Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
      [Report.InsertedWordCount, Report.RejectedWordCount,
       Report.SkippedPageCount]));
  finally
    Pdf.Free;
  end;
end;

SkipPagesWithText đáng được nhấn mạnh trong các kho lưu trữ hỗn hợp. Một PDF đã sẵn có văn bản thật, dù sinh ra đã là văn bản số hay đã được xử lý trước đó, sẽ nhận thêm một lớp văn bản thứ hai nếu bạn chạy OCR lên nó một cách mù quáng, và bản trùng lặp khiến việc trích xuất trả về mỗi từ hai lần. Trạng thái từng trang popsSkippedExistingText cho bạn biết chính xác trang nào đã được để yên

Ngân sách, hủy bỏ và ngăn chặn thất bại

Mọi đại lượng mà một tài liệu độc hại hoặc chỉ đơn thuần khổng lồ có thể thổi phồng đều có một trần: điểm ảnh trên mỗi trang và tổng thể, số từ trên mỗi trang và tổng thể, và số ký tự trên mỗi từ. Tất cả đều được kiểm tra trước khi trang được ghi ra, không phải sau đó, và ước tính điểm ảnh được tính từ kích thước trang và DPI trước khi bất kỳ bitmap nào được cấp phát. Tăng DPI từ 150 lên 300 làm tăng bộ nhớ trên mỗi trang lên gấp bốn lần, nên trần trên mỗi trang là tham số cần điều chỉnh trước tiên khi một tác vụ hàng loạt bắt đầu thất bại trên các khổ giấy lớn

Token hủy bỏ xuyên suốt toàn bộ đường xử lý: render tiến triển, lệnh gọi bộ cung cấp và vòng lặp chèn từng từ. Điều đó nghĩa là một người dùng hủy bỏ giữa lúc nhận diện một file 400 trang sẽ dừng lại trong vòng một trang thay vì đến cuối tài liệu, và cùng mẫu token đó được dùng ở nơi khác trong thành phần, được mô tả trong render tiến triển có thể hủy bỏ, cũng áp dụng ở đây không thay đổi

Việc ngăn chặn thất bại diễn ra theo từng trang. Thư viện thu thập các handle đối tượng nó đã chèn trên một trang và gọi FPDFPage_GenerateContent một lần, sau khi mọi từ đã được đặt xong. Nếu có gì thất bại giữa chừng, dù là lỗi bộ cung cấp hay vấn đề về font, các đối tượng đã chèn trên trang đó bị gỡ theo thứ tự ngược lại và nội dung trang được tạo lại, nên một trang thất bại quay về trạng thái gốc thay vì giữ lại một nửa lớp văn bản. Vòng lặp tài liệu sau đó tiếp tục hoặc dừng lại theo ContinueOnError, và trang đang hoạt động luôn được khôi phục

Xác minh rằng hình ảnh thực sự không bị chạm vào

Kiểm tra mạnh nhất có sẵn cũng đơn giản nhất: render trang trước và sau khi áp dụng lớp văn bản ở cùng kích thước rồi so sánh các bitmap. Chúng phải giống hệt nhau từng byte, vì văn bản vô hình không vẽ ra gì và luồng hình ảnh chưa bao giờ bị giải mã. Bất kỳ khác biệt nào có nghĩa là có gì đó khác ngoài lớp văn bản đã thay đổi trang

Sau đó, hãy kiểm chứng phía văn bản bằng cách trích xuất từ file đã xử lý và xác nhận rằng vị trí các từ nằm đúng trên bản scan. Đường dẫn trích xuất giống với đường dẫn được mô tả trong trích xuất văn bản từ tài liệu PDF, và để kiểm tra căn chỉnh trực quan nhanh, render trang thành hình ảnh như trong chuyển đổi trang PDF sang JPEG cho phép bạn phủ hộp từ lên trên bản scan

Xếp lớp OCR, render, trích xuất và chỉnh sửa đều chạy trên cùng một đối tượng tài liệu trong Delphi, C++Builder và Lazarus; toàn bộ bề mặt API được mô tả trên trang PDFium Component cho Delphi