Bài viết kỹ thuật

Trích xuất văn bản, hình ảnh và font từ PDF trong Delphi bằng PDF Library for Delphi

Moi văn bản, ảnh và font ra khỏi một tệp PDF sẵn có nghe như bài toán đã giải xong, cho tới khi bạn cho một kho tài liệu thật chạy qua nó. Chĩa một bộ đánh chỉ mục tìm kiếm vào bốn mươi nghìn tệp của khách hàng thì các kiểu hỏng phân thành vài đống nhận diện được. Chữ dính liền vào nhau vì chẳng ai bảo bộ trích xuất rằng khoảng trống rộng bao nhiêu thì tính là một dấu cách. Những trang khác trả về toàn ký tự vô nghĩa vì một font đã tập con hóa không mang theo bản đồ từ mã glyph của nó về ký tự thật. Và "logo công ty" hóa ra là chín đối tượng ảnh riêng biệt xếp chồng sau một soft mask. Chẳng cái nào trong đó là lỗi của thư viện. Đó là khác biệt giữa việc gọi một hàm trích xuất với việc hiểu hàm đó khôi phục được và không khôi phục được gì từ tập byte trên đĩa

losLab PDF Library, ấn bản Pascal, cho mã Delphi và C++Builder nhiều hơn một cách để đọc mỗi luồng trong ba luồng đó, và các mức khác nhau ở chỗ chúng bảo đảm được gì. Mẹo nằm ở việc khớp mức với công việc: một bộ đánh chỉ mục tìm kiếm, một trình duyệt xét bôi xóa và một lượt preflight PDF/A đều muốn những thứ khác nhau từ cùng một trang, và với tay tới sai lệnh gọi thì hoặc phí công hoặc cho ra kết quả bạn không dám tin

Các mức trích xuất văn bản và lời hứa của từng mức

GetPageText nhận một giá trị tùy chọn từ 0 tới 8, và con số đó chọn một engine chứ không chọn một định dạng. Giá trị 0 tới 2 chạy một lượt nhẹ nhàng, đủ dùng cho một bản xem trước nhanh. Giá trị 3 tới 8 đi qua engine hiểu bố cục, engine này dựng lại dòng và khoảng cách từ chỗ các glyph thực sự nằm trên trang. Trong dải đó, các biến thể có ý nghĩa riêng: 4 và 6 tách đầu ra thành từng từ, 5 và 6 phát ra bề rộng theo từng glyph, còn 7 trả về văn bản thuần với phần metadata về font, màu và khối bị bỏ đi có chủ ý. Tùy chọn 7 là thứ nên đưa cho một bộ đánh chỉ mục tìm kiếm, vì bộ chỉ mục chỉ muốn từ ngữ chứ không muốn gì khác

Không thiết lập tùy chọn nào cứu nổi một tài liệu vốn chưa bao giờ mang theo thông tin cần thiết ngay từ đầu. PDF ánh xạ mã ký tự sang hình dạng glyph, và thứ duy nhất ánh xạ ngược những mã đó về văn bản đọc được là ToUnicode CMap của font (ISO 32000-1 §9.10). Khi một font đã tập con hóa xuất xưởng mà thiếu nó, mọi bộ trích xuất đều bó tay. Thư viện này, thao tác chép dán trong một trình xem, một bộ công cụ đối thủ: tất cả đều bị dồn xuống mức đoán mò theo tên glyph hoặc trả về rỗng. Cách đáp trả thực dụng là phát hiện chứ không phải làm anh hùng. Hãy chấm trang đó là độ tin cậy thấp rồi đẩy sang OCR, bởi lặng lẽ đưa rác vào chỉ mục còn tệ hơn việc thừa nhận rằng bạn không đọc nổi nó

Sơ đồ các cấp trích xuất văn bản PDF trong Delphi: các tùy chọn GetPageText từ 0 đến 8 định tuyến tới một lượt xử lý gọn nhẹ hoặc engine nhận thức bố cục, và các trang có phông subset thiếu ToUnicode CMap sẽ chuyển sang OCR
Các giá trị option 0 đến 8 của GetPageText chọn giữa lượt preview nhẹ nhàng và engine nhận biết layout, với option 7 dành cho chỉ mục tìm kiếm và những CMap ToUnicode thiếu vắng được chuyển sang OCR

Với những trường hợp mà các tùy chọn phẳng không bao được, như tách token tùy chỉnh, pháp y luồng nội dung, một phễu lọc văn bản dựng theo luật riêng của bạn, thì bộ giải mã có sẵn ở tầng dưới. TPDFExtractor được dựng trên resources dictionary và bộ font của một trang. Phương thức ExtractTextW của nó chạy các thao tác văn bản thô của luồng nội dung ngược qua chính cỗ máy font đó để khôi phục Unicode, còn sự kiện OnFindObject trao cho bạn từng đối tượng khi chúng trôi qua. Phần lớn mã chẳng bao giờ cần với sâu tới đây. Những ứng dụng cần tới lại là những ứng dụng mừng vì tầng này công khai chứ không bị chôn kín

Khối có tọa độ: đơn vị của kết quả tìm kiếm và việc xét bôi xóa

Văn bản thuần cho bạn biết trang nói gì. Sớm muộn một sản phẩm còn cần biết trang nói điều đó ở đâu, để tô sáng một kết quả tìm kiếm, khoanh khung quanh một ứng viên bôi xóa, hay neo một annotation vào đúng chỗ. ExtractPageTextBlocks trả về một handle trỏ tới danh sách các đoạn văn bản, và mỗi đoạn mang theo nội dung, hộp bao cùng tên font và cỡ chữ mà nó được đặt:

var
  Pdf: TPDFlib;
  Blocks, I: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Pdf.SelectPage(1);
    Blocks := Pdf.ExtractPageTextBlocks(0);
    for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
      Writeln(Format('%s  [%s %.1f pt at %.0f,%.0f]',
        [Pdf.GetTextBlockText(Blocks, I),
         Pdf.GetTextBlockFontName(Blocks, I),
         Pdf.GetTextBlockFontSize(Blocks, I),
         Pdf.GetTextBlockBound(Blocks, I, 0),
         Pdf.GetTextBlockBound(Blocks, I, 1)]));
    Pdf.ReleaseTextBlocks(Blocks);
  finally
    Pdf.Free;
  end;
end;

Một chi tiết ở khu vực này làm vấp các bản tích hợp nhiều hơn bất cứ chi tiết nào khác. SetTextExtractionArea, SetTextExtractionWordGapSetTextExtractionOptions là trạng thái ở cấp tài liệu và tồn tại dai, chứ không phải đối số bạn truyền theo từng lệnh gọi. Cấu hình một giới hạn vùng cho một tính năng, chẳng hạn chỉ đọc dải đầu trang để phân loại tài liệu, thì nó lặng lẽ cắt cụt mọi lượt trích xuất sau đó trên cùng handle, kể cả các mức GetPageText hiểu bố cục mà bạn với tay tới về sau. Hoặc hãy đặt lại trạng thái trích xuất giữa các nhiệm vụ lô-gic, hoặc cấp cho mỗi nhiệm vụ một handle tài liệu riêng

Ngưỡng khoảng cách từ chính là cần gạt cho cái đống hỏng đầu tiên, tức những chữ dính liền nhau. SetTextExtractionWordGap bảo engine bố cục rằng bao nhiêu khoảng trống theo chiều ngang, đo theo chính khoảng cách glyph của trang, thì tách từ này với từ kế tiếp. Một bảng dày đặc cần khoảng nhỏ hơn một trang tiếp thị dàn thưa, nên một ngưỡng tinh chỉnh theo từng lớp tài liệu hơn hẳn một hằng số toàn cục. Nó tồn tại dai trên tài liệu như phần còn lại của trạng thái trích xuất, nên hãy tính đến việc đặt nó một cách có chủ ý thay vì đặt một lần rồi quên

Sơ đồ cho thấy trạng thái trích xuất PDF cấp tài liệu trong Delphi được giữ qua các lượt gọi trên cùng một handle cho đến khi reset, điều này ngăn việc cắt cụt âm thầm các lần trích xuất về sau
Vùng trích xuất, word-gap và các tùy chọn được lưu giữ trên handle tài liệu, nên một vùng đặt cho một tính năng âm thầm cắt ngắn mọi lần trích xuất sau đó cho tới khi trạng thái được đặt lại hoặc handle được thay

Ảnh: luồng dữ liệu gốc, không phải ảnh chụp màn hình

Cách sai để lấy ảnh ra khỏi một tệp PDF là kết xuất trang rồi cắt cúp. Cách đó lấy mẫu lại các điểm ảnh, nướng luôn mọi phép xoay vào ảnh, và vứt đi bản gốc vốn là gì. Thay vào đó, GetPageImageList liệt kê đúng những tài nguyên ảnh mà trang tham chiếu tới, và mỗi mục trả về các thuộc tính cùng dữ liệu gốc nguyên vẹn của nó:

var
  ImgList, I: Integer;
begin
  Pdf.SelectPage(1);
  ImgList := Pdf.GetPageImageList(0);
  for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
  begin
    Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
    Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
      Format('page1-img%.2d.bin', [I]));
  end;
  Pdf.ReleaseImageList(ImgList);
end;

Hãy kiểm tra GetImageListItemFormatDesc trước khi giả định bất cứ điều gì về một mục, bởi thứ mà một trang tham chiếu tới hiếm khi là một bức ảnh gọn ghẽ cho mỗi hình nhìn thấy được. Một soft mask hiện ra thành một mục riêng. Cùng một XObject thường lặp lại trên nhiều trang, nên hãy khử trùng lặp theo hàm băm nội dung trước khi lưu trữ một bản xuất "toàn bộ ảnh", kẻo bạn sẽ ghi cùng cái logo cả trăm lần. Ảnh JPEG hệ CMYK cần được quản lý màu ở hạ nguồn, không thì chúng hiện ra đảo màu trong những trình xem lấy các kênh màu theo đúng mặt chữ. Khi bạn muốn kiểm kê trên toàn tài liệu thay vì từng trang một, FindImages đi cùng SetFindImagesMode quét cả tệp trong một lượt

Có một ranh giới đáng nêu với các bên liên quan trước khi ai đó viết tiêu chí nghiệm thu: việc trích xuất ảnh chỉ trả về tài nguyên dạng raster. Một logo hay biểu đồ vẽ bằng đường vector không phải là ảnh theo nghĩa tài nguyên và sẽ không bao giờ xuất hiện trong bất kỳ danh sách ảnh nào, dù trên màn hình nó đọc lên rõ ràng là một bức hình. Khi yêu cầu thật sự là giao cái biểu đồ đó dưới dạng một tệp, cách trung thực là kết xuất vùng trang ấy ra một bitmap, và đó là thao tác khác với độ trung thực khác. Hai loại đầu ra này không nên nằm chung một thư mục xuất mà thiếu nhãn ghi rõ cái nào là cái nào

So sánh render một trang PDF Delphi để lấy ảnh với trích xuất các stream ảnh gốc bằng GetPageImageList, bao gồm các lưu ý về soft mask, XObject trùng lặp và CMYK
Render và crop resample pixel và vứt bỏ dữ liệu ảnh gốc, trong khi GetPageImageList liệt kê các tài nguyên ảnh đã lưu cùng thuộc tính và stream nguyên vẹn của chúng

Font: một bề mặt kiểm toán, không phải tính năng xuất tệp

API font trả lời các câu hỏi về font. Nó không trao cho bạn chính các tệp font, và sự phân biệt đó định hình mọi thứ bạn dựng được trên nó. Sau khi FindFonts quét tài liệu, phép liệt kê duyệt các font theo ID, và các lệnh gọi thuộc tính báo cáo về font đang được chọn:

var
  I: Integer;
begin
  Pdf.FindFonts;
  for I := 1 to Pdf.FontCount do        // chỉ số font bắt đầu từ 1, không phải 0
    if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
      Writeln(Format('%s  type=%d  embedded=%d  subset=%d',
        [Pdf.FontName, Pdf.FontType,
         Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;

Hãy để mắt tới cận của vòng lặp. Chỉ số font chạy từ 1 tới FontCount, trong khi chỉ số của khối văn bản và danh sách ảnh vài đoạn phía trên lại bắt đầu từ 0. Mang quy ước này sang quy ước kia là bạn có ngay một lỗi lệch một, hoặc bỏ sót font đầu tiên hoặc chạy vượt quá cuối danh sách, và nó sẽ qua được kiểu kiểm thử qua loa vì phần lớn tài liệu có vài font và cái sai vẫn trông có vẻ hợp lý. Cũng cần rõ ràng về phạm vi. API này không có chức năng xuất font ở mức byte. Không lệnh gọi nào trả về chương trình font nhúng dưới dạng tệp TTF hay OTF, và liệt kê cộng với soi metadata là toàn bộ mô hình đã dự tính. Mô hình đó vẫn bao được những gì công việc sản xuất thực sự cần ở font: phát hiện tập con hóa theo mẫu tên, kiểm toán việc nhúng trước một lượt chuyển đổi sang bản lưu trữ (một font không nhúng là rào chặn cứng với PDF/A, như preflight PDF/A và PDF/UA trong Delphi có đi vào), và chẩn đoán cách mã hóa cho lúc độ tin cậy trích xuất tụt xuống. Cũng có một lý do về bản quyền khiến ranh giới nằm ở đây. Một chương trình font tập con là tư liệu có giấy phép và, khi đã thiếu phần lớn glyph, dù sao cũng vô dụng nếu đem cài. Coi nó là metadata phục vụ kiểm toán chứ không phải tài sản trích xuất được mới là lập trường bạn bảo vệ nổi

Lệnh gọi cuối cùng ấy làm tròn phận sự trong khâu phân loại. Hãy chạy GetFontEncoding trên từng font, đọc nó cùng với cờ tập con hóa, và bạn dự đoán được chất lượng trích xuất trước khi rút ra một ký tự nào. Một trang mà mọi font đều đã tập con hóa với cách mã hóa phi tiêu chuẩn thì chỉ nhìn thôi đã thành ứng viên cho OCR, nhờ đó một pipeline chạy lô định tuyến nó đúng chỗ mà không phải phí một lượt trích xuất thất bại trước đã

Trích xuất ở quy mô lớn mà không nạp tài liệu

Trong một pipeline chạy lô, nạp cả tài liệu chỉ để đọc một trang là phí I/O, và trên cả một kho tài liệu thì nó cộng dồn rất nhanh. Các biến thể gọi một lần, tức ExtractFilePageTextExtractFilePageTextBlocks, nhận thẳng tên tệp, mật khẩu và số trang rồi bỏ qua lượt nạp đầy đủ. Với các tệp cỡ gigabyte còn một số truyền thấp hơn nữa. Đường direct-access mở tệp bằng cách đọc xref theo lối streaming, nên DAOpenFileReadOnly rồi tới DAExtractPageText chỉ chạm vào những đối tượng mà đúng trang đó cần. Nó đi kèm một thay đổi quy ước đáng thuộc nằm lòng: các hàm DA địa chỉ hóa trang bằng PageRef, tức một handle tham chiếu đối tượng bạn lấy từ DAFindPage, chứ không bao giờ bằng số trang thô. Truyền con số vào chỗ dành cho handle thì lệnh gọi thao tác lên sai đối tượng mà chẳng báo lỗi gì, và đó là kiểu sai lầm khó gỡ nhất. Phần còn lại của bộ đồ nghề direct-access được bày ra trong gộp, tách PDF cỡ lớn và direct access

Nếu có một thói quen duy nhất phân biệt mã trích xuất sống sót qua một kho tài liệu thật với mã lết đi khập khiễng, thì đó là coi trang giấy như đầu vào không đáng tin thay vì một nguồn dữ liệu sạch sẽ. Văn bản lệch với thứ trình xem hiển thị gần như luôn là vấn đề mã hóa, một liên tự sụp thành một glyph hoặc một font tập con thiếu các mục ToUnicode, và cách chữa là đo độ tin cậy rồi chuyển hướng các trang xấu sang OCR, chứ không phải vật lộn với đám byte. API font sẽ không bao giờ sinh ra TTF hay OTF, đó là chủ ý thiết kế, nên hãy dựng các luồng công việc về font quanh những câu hỏi kiểm toán. Còn trạng thái trích xuất tồn tại dai, nhất là hình chữ nhật giới hạn vùng, là một thiết lập bạn sở hữu suốt vòng đời của một handle tài liệu, không phải tham số bạn quên đi sau một lệnh gọi. Nắm đúng ba phản xạ đó thì phần còn lại của API sẽ ngoan ngoãn

Bản dựng để đánh giá, các dự án demo và tài liệu API trích xuất đầy đủ nằm trên trang sản phẩm losLab PDF Library for Delphi