HotPDF Component trích xuất văn bản Unicode từ bất kỳ tệp PDF nào bạn tải trong Delphi thông qua hai lệnh gọi: ExtractLoadedPageText trả về văn bản theo luồng đọc của trang, và ExtractLoadedPageTextLayout (được thêm từ phiên bản v2.263.0) tái dựng lại sự sắp xếp trực quan của trang dưới dạng văn bản thuần, giúp giữ lại các cột, khoảng thụt lề và sự căn hàng của bảng trong kết quả đầu ra. Cả hai đều hoạt động trên các tài liệu mà HotPDF không tự tạo ra, đây chính là trường hợp thực tế quan trọng nhất: hóa đơn khách hàng gửi qua email cho bạn, báo cáo do đơn vị quét tài liệu cung cấp, hoặc hợp đồng được tạo bởi một phần mềm mà không ai nhớ tên nữa
Để làm được điều đó đòi hỏi nhiều cơ chế phức tạp hơn những gì hai chữ ký hàm thể hiện, bởi vì tệp PDF không lưu trữ văn bản giống như một tệp văn bản thuần túy. Bài viết này sẽ hướng dẫn cả hai chế độ trích xuất, sau đó làm rõ ba thành phần cốt lõi bên dưới — trình đọc CMap, trình thông dịch luồng nội dung, và chuỗi giải mã phông chữ dự phòng — vì việc hiểu cách ánh xạ hoạt động là ranh giới giữa việc chấp nhận kết quả đầu ra bị lỗi và việc chẩn đoán được nguyên nhân lỗi
Tại sao trích xuất văn bản lại khó hơn việc đọc các chuỗi trực tiếp từ tệp tin?
Một luồng nội dung PDF ghi lại các mã ký tự, chứ không phải bản thân ký tự. Các toán tử Tj và TJ (ISO 32000-1 §9.4.3) mang các chuỗi byte mà ý nghĩa của chúng hoàn toàn phụ thuộc vào phông chữ được chọn bởi toán tử Tf đứng trước nó: byte 0x41 có thể là chữ cái A trong bảng mã WinAnsi, một ký tự đồ họa (glyph) tùy ý trong một phông chữ rút gọn, hoặc một nửa của mã CID hai byte trong một phông chữ hỗn hợp CJK. Tiêu chuẩn ISO 32000-1 §9.10 định nghĩa việc trích xuất văn bản chính xác là bài toán giải mã này — ánh xạ mỗi mã ký tự trở lại Unicode bằng cách sử dụng bất kỳ thông tin nào mà từ điển phông chữ cung cấp — và tiêu chuẩn cũng nêu rõ rằng một tệp tin tuân thủ chuẩn không bắt buộc phải cung cấp đủ thông tin để làm việc đó
Điều khoản cuối cùng đó giải thích cho mọi báo cáo lỗi "tại sao sao chép-dán từ tệp PDF này lại ra ký tự rác" mà bạn từng gặp. Một trình tạo PDF nhúng một phông chữ rút gọn không có bảng /ToUnicode đã tạo ra một tệp hiển thị hoàn hảo nhưng trích xuất ra văn bản vô nghĩa, bởi vì ánh xạ từ mã sang ký tự đồ họa thì có nhưng ánh xạ từ mã sang Unicode thì không bao giờ được gửi kèm. Do đó, bất kỳ API trích xuất trung thực nào cũng là một chuỗi các giải pháp dự phòng nỗ lực tối đa, và câu hỏi hữu ích ở đây là chuỗi dự phòng đó đi sâu đến mức nào
Trích xuất theo luồng đọc với ExtractLoadedPageText
Để phục vụ việc lập chỉ mục tìm kiếm, khớp từ khóa hoặc đưa văn bản vào quy trình phân tích dữ liệu, ExtractLoadedPageText là lệnh gọi bạn cần. Chữ ký hàm là function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — các chỉ số trang tính từ 0, kết quả trả về dưới dạng một chuỗi UnicodeString gốc của Delphi, và hàm trả về False khi trang không có luồng nội dung có thể đọc được thay vì kích hoạt một ngoại lệ
var
Pdf: THotPDF;
PageCount, I: Integer;
PageText, AllText: UnicodeString;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('invoice.pdf');
AllText := '';
for I := 0 to PageCount - 1 do
if Pdf.ExtractLoadedPageText(I, PageText) then
AllText := AllText + PageText + #13#10;
// AllText now holds the reading-flow text of the document
finally
Pdf.Free;
end;
end;
Việc xuống dòng trong văn bản đầu ra được thực hiện dựa trên một thuật toán phán đoán đơn giản: khi gốc tọa độ dọc của một ký tự dịch chuyển nhiều hơn một nửa kích thước phông chữ hiện tại — dấu hiệu của bước di chuyển Td hoặc T* trong luồng nội dung — một ký tự xuống dòng sẽ được chèn vào. Các ký tự mà bộ giải mã không thể phân giải sẽ được thay thế bằng khoảng trắng thay vì biến mất hoàn toàn, giúp ranh giới từ vẫn được bảo toàn ngay cả khi các ký tự riêng lẻ bị lỗi. Chế độ này không cố gắng phát hiện thứ tự đọc hoặc phân tách nhiều cột: một trang có hai cột sẽ xuất ra nội dung xen kẽ theo thứ tự của luồng nội dung, thường là nhưng không phải luôn luôn trùng với thứ tự hiển thị trực quan
Khi nào bạn nên sử dụng trích xuất bảo toàn bố cục?
ExtractLoadedPageTextLayout là lệnh gọi phù hợp bất cứ khi nào vị trí mang ý nghĩa: bảng biểu, biểu mẫu, danh sách mã nguồn, hoặc bất kỳ thứ gì bạn muốn so sánh (diff), lọc tìm (grep) hoặc phân tích cú pháp theo cột. Thay vì làm phẳng các ký tự đồ họa thành một luồng văn bản đơn thuần, nó nhóm chúng vào các đường cơ sở (baselines), sắp xếp từng đường cơ sở theo trục X, và tái tạo khoảng trắng ngang cũng như dọc trên một lưới ký tự đơn cách (monospaced grid) có kích thước dựa trên khoảng tăng trung vị của ký tự và kích thước phông chữ. Những khoảng trống rộng giữa các khối văn bản trên cùng một đường cơ sở sẽ trở thành các khoảng trắng; những khoảng trống lớn giữa các đường cơ sở trở thành các dòng trống. Kết quả văn bản đọc được sẽ giống hệt như cách trang giấy hiển thị
var
Grid: UnicodeString;
begin
if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
// Columns, indentation and table alignment survive as
// spaces and blank lines on a character grid
end;
Cả hai chế độ đều chia sẻ chung toàn bộ cơ chế giải mã bên dưới và chỉ khác nhau ở cách sắp xếp các ký tự đã được giải mã, vì vậy việc lựa chọn chế độ này hay chế độ kia không làm giảm độ chính xác của dữ liệu. Hãy chọn ExtractLoadedPageText khi bạn chỉ quan tâm đến câu chữ và chọn ExtractLoadedPageTextLayout khi cách bố trí trực quan là quan trọng. Việc phát hiện thứ tự đọc đa cột vẫn nằm ngoài phạm vi xử lý của cả hai chế độ — kết quả hiển thị dạng lưới của một trang hai cột sẽ hiển thị cả hai cột song song cạnh nhau một cách trung thực, điều này hoàn hảo cho việc so sánh nội dung (diff) nhưng không phù hợp cho việc sắp xếp lại dòng chảy văn bản (re-flow)
HotPDF giải mã mã ký tự sang Unicode như thế nào?
HotPDF Component phân giải mỗi mã ký tự thông qua một chuỗi các bước dự phòng được sắp xếp theo thứ tự ưu tiên: đầu tiên là bảng /ToUnicode CMap được nhúng của phông chữ, tiếp theo là mục nhập /Encoding (luồng dữ liệu hoặc CMap được đặt tên), sau đó — đối với phông chữ hỗn hợp — là các tệp tin CMap chuẩn của Adobe dành cho các tập hợp ký tự như Adobe-GB1, Adobe-CNS1, Adobe-Japan1 và Adobe-KR, và cuối cùng là các bảng WinAnsi và MacRoman tích hợp sẵn cho các phông chữ đơn giản. Một phương án không thể đưa ra câu trả lời sẽ chuyển tiếp một cách âm thầm sang phương án tiếp theo thay vì gây ra lỗi, và một mã ký tự không thể phân giải được sau khi đi hết chuỗi dự phòng sẽ nhận giá trị là 0 để người gọi có thể thống kê số lượng ký tự bị lỗi thay vì phải phán đoán
Bảng /ToUnicode CMap (ISO 32000-1 §9.10.3) đứng ở vị trí ưu tiên đầu tiên vì nó là bảng ánh xạ được trình tạo PDF thiết lập riêng cho việc trích xuất văn bản. Đường dẫn CMap tiêu chuẩn của Adobe rất quan trọng đối với các tài liệu tiếng CJK sử dụng các bảng CMap được định nghĩa trước như UniGB-UTF16-H thay vì nhúng dữ liệu trực tiếp: HotPDF đi kèm các tệp bộ sưu tập này trong thư mục resources\CMap, xác định vị trí của chúng tương đối với tệp thực thi khi chạy, và lưu trữ tạm thời (cache) mỗi bản đồ đã phân tích theo từng tiến trình — điều này rất đáng lưu ý vì tệp lớn nhất trong số đó, bản đồ Adobe-GB1, có dung lượng văn bản nguồn khoảng 2 MB và bạn chắc chắn không muốn phân tích lại nó trên mỗi trang giấy. Nếu thư mục này không tồn tại, bộ giải mã chỉ đơn giản bỏ qua các CMap trên đĩa và làm việc với các bảng nhúng kèm theo các bảng mã tích hợp sẵn. Đây là phiên bản phía đọc của vấn đề định hình văn bản được trình bày trong bài viết về định hình văn bản chữ phức tạp với HotPDF, nơi sự phân biệt tương tự giữa mã ký tự và ký tự đồ họa được xử lý tại thời điểm ghi tài liệu
Hai bẫy cú pháp CMap rất đáng lưu ý
Các tệp CMap trông có vẻ dễ phân tích cú pháp nhưng thực tế lại không phải vậy, và hai chi tiết sau đây là nguyên nhân gây ra hầu hết các thất bại phân tích cú pháp trong lần thử đầu tiên. Chi tiết thứ nhất là số lượng bản ghi xuất hiện trước từ khóa phần: một phần sẽ được viết là 2 beginbfchar, chứ không phải beginbfchar 2. Một bộ phân tích cú pháp mong đợi số lượng đứng sau từ khóa sẽ coi con số đó là một token rác, và sau đó sẽ tìm thấy không có mục nhập nào trong mỗi phần. Cách tiếp cận mạnh mẽ — cách mà trình đọc của HotPDF áp dụng — là bỏ qua hoàn toàn số lượng bản ghi đó và lặp cho đến khi gặp từ khóa đóng endbfchar / endbfrange tương ứng, điều này còn giúp xử lý được cả các tệp lỗi trong thực tế có ghi sai số lượng bản ghi
Cái bẫy thứ hai là các đích đến bfchar và bfrange là các chuỗi UTF-16BE, chứ không phải số nguyên. Đích đến <D83DDE00> biểu thị U+1F600 — một cặp thay thế (surrogate pair) cần phải được kết hợp lại thành một điểm mã duy nhất — và việc đọc bốn byte đó dưới dạng số nguyên big-endian sẽ tạo ra một giá trị vô nghĩa đối với mọi điểm mã nằm ngoài Basic Multilingual Plane (Mặt phẳng đa ngôn ngữ cơ bản). Emoji trong tệp PDF ngày nay không còn là điều hiếm gặp, vì vậy bộ giải mã bỏ qua việc kết hợp lại các cặp thay thế sẽ thất bại trên các tệp thực tế của người dùng. HotPDF phân tích cú pháp chuỗi hex thành các byte thô trước, sau đó kết hợp lại các đơn vị mã UTF-16BE, giúp xử lý được cả các đích đến gồm nhiều ký tự do ánh xạ chữ ghép (ligature mappings) tạo ra
Đi sâu vào cấp độ ký tự đồ họa với ExtractLoadedPageGlyphs
Cả hai lệnh gọi văn bản đều được xây dựng trên nền tảng của ExtractLoadedPageGlyphs, và mảng dữ liệu THPDFGlyphArray bên dưới cũng được mở để mã nguồn của bạn có thể sử dụng trực tiếp. Mỗi bản ghi THPDFGlyphRecord mang điểm mã Unicode đã phân giải cùng với mã ký tự thô, độ rộng byte của mã (1, 2 hoặc 4, được quyết định bởi trường codespacerange của CMap), khóa và kích thước của tài nguyên phông chữ đang hoạt động, tọa độ gốc X và Y trong không gian người dùng, và khoảng tăng ngang (horizontal advance). Thông tin đó là đủ để bạn xây dựng bộ phát hiện ranh giới từ, tô sáng văn bản theo vị trí hoặc một thuật toán bố cục tùy chỉnh mà không cần tự xử lý luồng nội dung
var
Glyphs: THPDFGlyphArray;
I, Unresolved: Integer;
begin
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
begin
Unresolved := 0;
for I := 0 to High(Glyphs) do
if Glyphs[I].Unicode = 0 then
Inc(Unresolved);
if Unresolved > 0 then
ShowMessageFmt('%d of %d glyphs have no Unicode mapping',
[Unresolved, Length(Glyphs)]);
end;
end;
Việc thống kê các bản ghi có Unicode = 0, như ví dụ trên, là cách thực tế nhất để đánh giá chất lượng trích xuất trên một tài liệu cụ thể trước khi tin tưởng sử dụng dữ liệu văn bản ở hạ nguồn quy trình. Các bản ghi ký tự đồ họa cũng neo giữ từng ký tự với toán hạng nguồn trong luồng nội dung, đó chính là cơ sở giúp HotPDF thực hiện được tính năng tìm kiếm và thay thế văn bản trên tài liệu đã tải từ cùng một nền tảng
Những điều mà ba lượt xử lý không thực hiện
Một số tệp tin sẽ vô hiệu hóa mọi bộ trích xuất, và tốt nhất là bạn nên phát hiện ra chúng thay vì cố xuất dữ liệu lỗi. Các tài liệu được quét là ví dụ rõ ràng nhất: một trang tài liệu chỉ là một hình ảnh lớn sẽ không chứa bất kỳ toán tử văn bản nào, vì vậy thao tác trích xuất sẽ trả về một chuỗi rỗng — giải pháp là OCR, và bước đầu tiên của quy trình đó là trích xuất hình ảnh của trang từ tệp PDF đã tải. Trường hợp khó hơn là các phông chữ rút gọn không có bảng /ToUnicode: nếu đường dẫn /Encoding và các CMap tiêu chuẩn cũng không có kết quả, các ký tự đồ họa đó sẽ được phân giải về giá trị 0 và hiển thị dưới dạng khoảng trắng trong các lệnh gọi trích xuất văn bản. Các tài liệu được mã hóa vẫn trích xuất bình thường miễn là bạn tải chúng kèm theo mật khẩu thông qua phương thức nạp chồng LoadFromFile, giúp giải mã các luồng dữ liệu trước khi trình thông dịch xử lý chúng
Một giới hạn nhỏ hơn cần được nêu rõ: chuỗi giải mã đọc các luồng CMap và nội dung qua đường dẫn Flate của HotPDF, do đó một phông chữ có luồng dữ liệu ToUnicode sử dụng một bộ lọc bất thường sẽ chuyển đổi sang phương án dự phòng tiếp theo thay vì làm hỏng toàn bộ trang giấy. Trong thực tế, FlateDecode hỗ trợ hầu hết mọi tài liệu được tạo ra trong hai thập kỷ qua, và việc chuyển đổi dự phòng diễn ra âm thầm theo thiết kế — bạn sẽ nhận được văn bản tốt nhất mà tệp tin cho phép thay vì gặp lỗi ngoại lệ. Cùng một cơ chế xử lý đối tượng phía đọc giúp phân giải từ điển phông chữ ở đây cũng là nền tảng cho tính năng chỉnh sửa metadata trên tài liệu đã tải, giúp luồng tiếp nhận tài liệu có thể trích xuất, kiểm tra và chú thích trong một lượt xử lý duy nhất
Trích xuất văn bản, kết xuất bảo toàn bố cục, truy cập cấp độ ký tự đồ họa, và các tính năng tìm kiếm và thay thế được xây dựng trên đó đều là một phần của tiêu chuẩn HotPDF Component dành cho Delphi và C++Builder — không sử dụng DLL bên ngoài, không cần dịch vụ văn bản của hệ điều hành, tất cả chỉ là mã nguồn Object Pascal mà bạn có thể duyệt qua từng bước gỡ lỗi khi gặp một tệp lạ trong hàng đợi