Việc trích xuất văn bản PDF có vẻ đơn giản cho đến khi bạn gặp một tài liệu mà lớp văn bản bị thiếu, bị hỏng, hoặc bị chia thành hàng chục đoạn ký tự nhỏ mà không có thứ tự có ý nghĩa. PDFium Component cung cấp cho bạn hai điểm nhập: mảng Character[] để truy cập dựa trên chỉ mục thô vào từng glyph trên một trang, và ReadablePageContent cho một dạng xem có cấu trúc tái tạo lại các đoạn văn và tiêu đề từ cây thẻ của PDF hoặc phân tích suy luận. Không phải cái nào cũng luôn là lựa chọn đúng, vì vậy việc hiểu rõ từng cái tiết lộ điều gì là rất quan trọng
Mở tài liệu và bẫy lỗi im lặng
TPdf mở một tệp bằng cách đặt FileName và bật Active := True. Chi tiết quan trọng: Active := True không bao giờ ném ra ngoại lệ. Nếu tệp bị thiếu, được bảo vệ bằng mật khẩu hoặc bị hỏng, PDFium sẽ bắt lỗi bên trong và Active chỉ đơn giản là vẫn ở trạng thái False. Điều đó có nghĩa là mọi vòng lặp trích xuất đều phải đề phòng điều này:
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True;
if not Pdf.Active then
begin
ShowMessage('Could not open PDF (damaged or wrong password)');
Exit;
end;
// extraction follows here
finally
Pdf.Active := False;
Pdf.Free;
end;
Các tệp được bảo vệ bằng mật khẩu cần thiết lập Pdf.Password := '...' trước Active := True. Không có cơ hội thứ hai: một khi Active thất bại, bạn đóng lại và mở lại bằng mật khẩu đúng
Trích xuất theo từng trang bằng Character[]
Cách tiếp cận cấp thấp nhất là duyệt qua mọi ký tự trên mỗi trang. Đặt Pdf.PageNumber để tải lớp văn bản cho trang đó, sau đó lặp qua các mục nhập CharacterCount bằng cách sử dụng thuộc tính Character[]. Có hai cờ trên mỗi mục nhập đáng để kiểm tra: CharacterGenerated[i] đánh dấu các glyph tổng hợp được chèn vào bởi bộ kết xuất (ví dụ như dấu gạch nối mềm tại các ngắt dòng) không có giá trị Unicode thực sự, và CharacterMapError[i] báo hiệu rằng PDFium không thể ánh xạ glyph đó tới một điểm mã (code point), điều này xảy ra với các mã hóa phông chữ thiếu bảng ToUnicode
procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
Page, I: Integer;
Line: string;
Ch: WideChar;
begin
for Page := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := Page;
Line := '';
for I := 0 to Pdf.CharacterCount - 1 do
begin
if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
Continue;
Ch := Pdf.Character[I];
if Ch = #13 then
Ch := #10; // normalize CR to LF
Line := Line + Ch;
end;
Output.Add(Line);
end;
end;
Kết quả là một chuỗi phẳng của các điểm mã Unicode theo thứ tự PDFium liệt kê chúng, đó là thứ tự chúng xuất hiện trong luồng nội dung, không nhất thiết phải là thứ tự đọc từ trái sang phải. Đối với hầu hết các tài liệu kịch bản chữ Latinh do các công cụ văn phòng tiêu chuẩn tạo ra, điều này là ổn. Đối với các tệp PDF được quét đã được OCR với các chuỗi glyph bất thường, hoặc cho văn bản từ phải sang trái, thứ tự có thể sai. Đó là lúc ReadablePageContent trở nên hữu ích hơn
Trích xuất có cấu trúc bằng ReadablePageContent
ReadablePageContent đi lên một cấp độ cao hơn: nó trả về một bản ghi TPdfReadableContent mà mảng Fragments của nó mang các phân đoạn nội dung được gắn thẻ, mỗi cái có một Kind xác định đó là đoạn văn, tiêu đề, mục danh sách, ô trong bảng, và v.v. Khi PDF mang một cây cấu trúc (kiểm tra Pdf.IsTagged), nguồn là rosStructure và thứ tự đọc là có thẩm quyền. Đối với các tệp không được gắn thẻ, PDFium rơi vào trường hợp rosHeuristic, nhóm các ký tự theo hộp bao quanh (bounding box) của chúng thành các đơn vị đọc hợp lý nhưng không thể đảm bảo độ chính xác
procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
Page: Integer;
Content: TPdfReadableContent;
Fragment: TPdfContentFragment;
begin
for Page := 1 to Pdf.PageCount do
begin
Content := Pdf.ReadablePageContent(Page);
for Fragment in Content.Fragments do
begin
case Fragment.Kind of
cfHeading : Output.Add('# ' + Fragment.Text);
cfParagraph : Output.Add(Fragment.Text);
cfListItem : Output.Add('- ' + Fragment.Text);
else
Output.Add(Fragment.Text);
end;
end;
end;
end;
Nếu Content.Source = rosHeuristic và đầu ra của bạn trông lộn xộn, lớp văn bản của tài liệu có lẽ không được viết với tâm trí về thứ tự đọc. Lúc đó cách khắc phục đáng tin cậy duy nhất là xuất lại từ ứng dụng nguồn với cấu trúc thẻ thích hợp, hoặc chạy một bước xử lý hậu kỳ sắp xếp nguồn gốc ký tự theo trục Y rồi tới trục X
CharacterOrigin và CharacterRectangle cung cấp cho bạn điều gì
Cả hai thuộc tính đều trả về vị trí của một ký tự trong không gian trang (point, gốc tọa độ ở góc dưới cùng bên trái, Y tăng dần lên trên). CharacterOrigin[i] là điểm neo đường cơ sở (baseline) của glyph; CharacterRectangle[i] là toàn bộ hộp bao quanh. Đây là những khối xây dựng cho bất kỳ thứ gì vượt ra ngoài văn bản thuần túy: phát hiện ranh giới cột, nhóm các ký tự thành các dòng bằng cách so sánh các tọa độ Y trong một khoảng dung sai, hoặc xây dựng một bản đồ kiểm tra chạm (hit-test) cho việc chọn văn bản trong trình xem. Nếu bạn cần tìm ký tự nào nằm dưới một cú nhấp chuột, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) sẽ thực hiện việc tra cứu đó trực tiếp mà bạn không cần phải lặp qua các hình chữ nhật
Đưa DLL vào đúng vị trí
PDFium Component ủy quyền toàn bộ việc phân tích PDF cho một DLL gốc (native), có thể là pdfium32.dll hoặc pdfium64.dll tùy thuộc vào nền tảng mục tiêu của bạn. Thành phần đi kèm với một tập lệnh CopyDlls.bat để sao chép đúng tệp vào thư mục hệ thống Windows. Việc chạy nó bằng quyền Administrator một lần trên máy phát triển là đủ; để triển khai, bạn sao chép DLL dọc theo tập tin thực thi của ứng dụng thay thế. Các biến thể có kích hoạt V8 (pdfium32v8.dll, pdfium64v8.dll) có kích thước lớn hơn đáng kể và chỉ cần thiết nếu các PDF của bạn chứa JavaScript cần được thực thi. Đối với việc trích xuất văn bản thuần túy, bản dựng tiêu chuẩn là sự lựa chọn đúng đắn
Nếu DLL bị thiếu tại thời điểm chạy, Active := True sẽ thất bại một cách im lặng giống như đối với một tệp bị thiếu, bởi vì thành phần này tự bắt lỗi tải nội bộ. Luôn luôn kiểm tra trên một máy tính sạch trước khi giao hàng
Sử dụng FontSize[] cùng với Character[] cho phân tích bố cục
Vượt qua văn bản thuần túy, API mức ký tự phơi bày FontSize[i], trả về kích thước điểm (point size) đã kết xuất của mỗi glyph. Kết hợp với CharacterOrigin[i] và CharacterRectangle[i], điều này cho phép bạn phân biệt văn bản phần thân với các tiêu đề mà không cần dựa vào cây cấu trúc. Một đoạn ký tự nơi kích thước phông chữ tăng vọt lên trên một ngưỡng gần như chắc chắn là một tiêu đề trong một tài liệu không được gắn thẻ. Cùng kỹ thuật này áp dụng cho việc phát hiện các chú thích (caption - văn bản nhỏ bên dưới hộp giới hạn hình ảnh) hoặc các chú thích cuối trang (footnote - văn bản nhỏ gần cuối trang). Không cái nào trong số này đòi hỏi kết xuất đồ họa; cả ba thuộc tính đọc trực tiếp từ lớp văn bản mà PDFium xây dựng trong lúc gọi Active := True
Một sắc thái nhỏ: FontSize[i] phản ánh kích thước sau khi ma trận biến đổi hiện tại (CTM) của trang được áp dụng, vì vậy một tài liệu mà tác giả đã mở rộng toàn bộ trang sẽ báo cáo kích thước được điều chỉnh theo tỷ lệ. Nếu bạn đang so sánh kích thước trên các trang có kích thước trang khác nhau, hãy chuẩn hóa theo chiều cao MediaBox của mỗi trang trước khi đưa ra quyết định ngưỡng
Ghi đầu ra ra tệp
TStringList của Delphi xử lý đầu ra UTF-8 sạch sẽ kể từ XE. Đặt WriteBOM := False nếu bạn cần một tệp không có BOM (nhiều bộ xử lý phía sau (downstream consumers) bị nghẽn bởi BOM ở đầu):
var
Lines: TStringList;
begin
Lines := TStringList.Create;
try
ExtractAllText(Pdf, Lines);
Lines.WriteBOM := False;
Lines.SaveToFile('output.txt', TEncoding.UTF8);
finally
Lines.Free;
end;
end;
Đối với các tài liệu rất lớn nơi bộ nhớ là một mối quan tâm, hãy ghi trực tiếp vào một TStreamWriter với TEncoding.UTF8 bên trong vòng lặp trang thay vì tích lũy mọi thứ vào một danh sách trước
Các API Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent, và CharacterIndexAtPos được trình bày ở đây là một phần của PDFium Component cho Delphi và C++Builder