Bỏ qua để đến nội dung
getnextpdf.com

Vì sao văn bản trong PDF không thực sự là văn bản

Khi bạn đọc một PDF, bạn thấy các từ. Tệp không chứa các từ. Nó chứa các chỉ dẫn để tô các hình tại những tọa độ, và các hình ấy tình cờ trông giống chữ cái. Khoảng cách giữa những gì một PDF vẽ và những gì nó có nghĩa chính là lý do một trang có thể trông không tì vết mà vẫn sao chép ra thành thứ vô nghĩa. Trang này nói về khoảng cách đó, và về tấm bản đồ nhỏ, riêng biệt khép lại nó.

Mọi thứ bạn làm với một PDF với tư cách là văn bản — bôi chọn nó, sao chép nó, tìm kiếm trong nó, lập chỉ mục để truy xuất, đọc to nó bằng trình đọc màn hình — đều phụ thuộc vào việc khôi phục các ký tự mà tệp chưa bao giờ trực tiếp lưu trữ. Nếu việc khôi phục đó thất bại, sự thất bại là vô hình. Trang vẫn kết xuất. Không ai để ý cho đến khi có người sao chép một đoạn vào email và nhận về □□□□, hoặc tìm trong một hợp đồng 400 trang một điều khoản rõ ràng đang nằm ở đó mà chẳng tìm thấy gì.

Đó là một lớp lỗi tốn kém chính bởi nó sống sót qua mọi lần soát xét bằng mắt. Chẳng có gì để thấy cả. Tài liệu trông đầy thẩm quyền và, đối với mục đích máy móc, lại câm lặng.

  • Một PDF vẽ các glyph — các hình thị giác được chọn bởi các mã số trỏ vào một font — chứ không phải các ký tự Unicode.
  • Cùng một hình có thể mang ý nghĩa của những ký tự khác nhau, và cùng một ký tự có thể được vẽ bằng những glyph khác nhau. Diện mạo và ý nghĩa được tách rời một cách có chủ đích.
  • Để lấy lại văn bản, một trình đọc chạy đường dẫn hiển thị theo chiều ngược lại: mã thành ký tự. CMap /ToUnicode của font là bảng tra cứu cho bước ngược đó (Spec: ISO 32000-2, §9.10).
  • Không có /ToUnicode, hoặc có một bản sai, nghĩa là sao chép-dán rối loạn và tìm kiếm thất bại — trong khi trang vẫn hoàn hảo đến từng điểm ảnh.
  • Đây là vấn đề ý nghĩa. Việc glyph có trông đúng hay không là vấn đề diện mạo riêng biệt được trình bày trong Fonts: the hard part. NextPDF phát ra một /ToUnicode đúng để vòng khứ hồi được trung thực.

Hãy bắt đầu từ chuyện văn bản lên trang ra sao. Một content stream không nói “viết từ file.” Nó chọn một font, rồi đưa một chuỗi cho một toán tử hiển thị văn bản (Spec: ISO 32000-2, §9). Mỗi mã là một chỉ số — một vị trí trong một chương trình font — và font biến chỉ số đó thành một đường viền glyph rồi tô nó ra. Mã 70 không phải là ký tự F. Nó là “khe 70 trong font này,” và khe 70 tình cờ chứa một đường cong hình F. Chọn một font khác thì khe 70 có thể là một bông tuyết.

Vậy nên một mã chỉ có ý nghĩa tương đối với encoding của font của nó. Với một font đơn giản thì encoding đó đại khái là một byte cho mỗi glyph. Với các hệ chữ cần đến hàng nghìn glyph — CJK, hoặc bất kỳ tài liệu full-Unicode nào — chừng đó không đủ chỗ, và PDF với tới một font tổng hợp (Type 0) (Spec: ISO 32000-2, §9.7). Một font tổng hợp đọc các mã nhiều byte qua một CMap, biến chúng thành các CID (định danh ký tự), rồi ánh xạ các CID thành glyph. Đó là một phép gián tiếp tinh tế cho phép một font đánh địa chỉ một tập glyph khổng lồ. Nó cũng là thêm một chỗ nữa mà dấu vết từ “cái được hiển thị” đến “cái nó có nghĩa” có thể nguội lạnh.

Giờ hãy chạy nó ngược lại. Để trích xuất văn bản, một trình đọc lấy các mã mà nó tìm thấy trong content stream và phải khôi phục các ký tự (Spec: ISO 32000-2, §9.10). Encoding đã vẽ glyph đi theo chiều ký tự đến glyph; việc trích xuất cần chiều mã glyph đến ký tự, và chiều đó không được bảo đảm là khả nghịch. Một font subset có thể đã đánh số lại các glyph của nó. Các CID của một font tổng hợp có thể là riêng tư. Hình trên trang không mang ý nghĩa Unicode cố hữu nào.

Cách khắc phục là gửi kèm bản đồ ngược cùng với font. Bản đồ đó là CMap /ToUnicode: với mỗi mã mà tài liệu sử dụng, nó ghi lại (các) giá trị Unicode mà mã đó đại diện cho. Có nó, việc trích xuất là một phép tra cứu gọn gàng. Không có nó, một trình đọc bị bỏ lại để đoán từ các encoding của font và các phép heuristic — và đoán chính xác là nơi mà fi trở thành một dấu hỏi.

  1. Your charactersThe Unicode text you set, for example the word 'file'.
  2. Encoding → codesThe font's encoding turns characters into numeric codes; a composite font routes them through a CMap to CIDs.
  3. Codes → glyphsEach code selects a glyph outline, which is painted to the page. This is the part you see.
  4. Extraction reverses itA reader reads the codes back and looks each one up in /ToUnicode to recover Unicode.
  5. Text againWith a correct /ToUnicode, copy, search, indexing, and screen readers all get the original characters back.
The two directions of PDF text. Rendering goes character → glyph and is what you see. Extraction goes code → character and is what you can copy, search, and read aloud — and it only works when the /ToUnicode map closes the loop.

NextPDF viết bản đồ ngược như một lẽ đương nhiên. Khi nó nhúng một font tổng hợp, nó phát ra hậu duệ CIDFontType2, cha Type0, và một CMap /ToUnicode để các mã phân giải ngược về Unicode — phần việc encoding được mô tả trong Fonts: the hard part. Điểm đáng tách bạch ở đây là vì sao: stream /ToUnicode không phải để làm cho trang trông đúng. Các glyph đã trông đúng mà không cần nó. Nó là artifact duy nhất giữ cho văn bản trích xuất được.

Nơi nổi tiếng nhất chuyện này lộ ra là ligature fi. Nhiều font vẽ fi thành một glyph kết hợp duy nhất, bởi dấu chấm của i va vào móc của f. Trên trang nó là một hình, được vẽ bởi một mã. Câu hỏi là điều gì xảy ra khi bạn sao chép nó.

% A /ToUnicode entry that maps the single ligature code to TWO characters,
% so selecting the 'fi' glyph copies out as 'f' then 'i' — not one mystery box.
1 beginbfchar
<0085> <00660069> % code 0x85 -> U+0066 'f' U+0069 'i'
endbfchar

Một mục đó là sự khác biệt giữa một lần tìm “file” có khớp và một lần tìm “file” âm thầm trượt mọi lần xuất hiện được kết xuất bằng ligature. Hãy ánh xạ mã ligature thành chuỗi hai ký tự f + i, và từ đó trở nên tìm được và sao chép được. Để nó không được ánh xạ, và trang vẫn hiển thị file hoàn hảo trong khi văn bản lặng lẽ chẳng nói điều gì máy có thể đọc.

Đây là lý do “fi” có thể trích xuất ra thành một ký tự hoặc hai, và vì sao sự khác biệt không hề ngẫu nhiên — nó là bất cứ điều gì bản đồ /ToUnicode được bảo phải nói. Một bản đồ đúng phân rã ligature ngược về các ký tự của nó. NextPDF phát ra chính xác loại mục đó, nên một ligature trên trang là hai chữ cái thông thường trên clipboard.

Cái bẫy, gọi đúng tên: “văn bản kết xuất ra được, vậy văn bản ổn.” Kết xuất và trích xuất là những cơ chế khác nhau đọc dữ liệu khác nhau. Kết xuất đi theo đường dẫn mã-đến-glyph và là thứ mắt bạn kiểm tra. Trích xuất đi theo đường dẫn mã-đến-ký-tự qua /ToUnicode và là thứ mọi bên tiêu thụ là máy móc kiểm tra. Một tài liệu có thể đạt điểm tuyệt đối ở việc đầu và thất bại hoàn toàn ở việc thứ hai, bởi vì về mặt cấu trúc chẳng có gì trực quan để lộ ra điều đó.

Một cái bẫy thứ hai, tinh vi hơn: cho rằng một glyph “hiển nhiên” biết nó là ký tự nào. Không phải vậy. Một glyph là một hình với một chỉ số. Phép ánh xạ ngược về một ký tự là thông tin bổ sung mà bên tạo ra phải cung cấp. Nếu bên tạo ra chưa bao giờ viết nó, không trình đọc nào có thể khôi phục nó một cách trung thực — nó chỉ có thể đoán.

Faithful text extraction from documents NextPDF writes — edition availability
EditionAvailability
CoreNextPDF emits a correct /ToUnicode CMap for the fonts it embeds, including the ligature and composite-font cases, so the documents it produces are searchable and copyable.
ProNot in this edition
EnterpriseNot in this edition

Một bản đồ /ToUnicode chỉ có thể mang thông tin mà font nguồn thực sự bộc lộ. Ở nơi một glyph không có ký tự xác định được — một dấu thuần túy trang trí, một biểu tượng vùng dùng riêng không có gán Unicode — không bản đồ nào có thể bịa ra ý nghĩa vốn chưa từng có. NextPDF phát ra phép ánh xạ trung thực mà nó có thể suy ra; nó không bịa ra các ký tự để lấp một khoảng trống.

Trang này nói về các tài liệu NextPDF viết ra. Nó không phải một công cụ sửa chữa cho các PDF đầu vào tùy ý mà /ToUnicode của chúng vốn đã thiếu hoặc sai; khôi phục văn bản từ những tệp đó là một bài toán riêng, mang tính heuristic. Và việc trích xuất trung thực là cần thiết cho khả năng tiếp cận nhưng không phải là toàn bộ của nó — thứ tự đọc, các thẻ, và văn bản thay thế được trình bày trong What makes a PDF accessible.

Vì sao cùng một PDF lại sao chép ổn trong trình đọc này mà tệ trong trình đọc khác? Các trình đọc khác nhau dự phòng theo những cách khác nhau khi /ToUnicode thiếu. Một số đoán từ encoding tích hợp của font và gặp may với văn bản Latin phổ biến; số khác thì không. Một /ToUnicode đúng loại bỏ trò xổ số — mọi trình đọc tuân thủ chuẩn đều nhận về cùng các ký tự.

Đây có phải là cùng một chuyện với việc font không được nhúng không? Không. Việc nhúng là về diện mạo — liệu glyph có tô được mà không cần cài font hay không. /ToUnicode là về ý nghĩa — liệu mã có ánh xạ ngược về một ký tự hay không. Một font có thể được nhúng hoàn hảo mà vẫn không có /ToUnicode dùng được, và đó chính là kiểu thất bại đẹp-mà-không-tìm-được.

Việc trích xuất có bao giờ cần nhiều hơn một ký tự cho mỗi mã không? Có — trường hợp ligature chính là vậy: một mã ánh xạ thành hai ký tự. Một mục /ToUnicode có thể ánh xạ một mã đơn thành một chuỗi ngắn, và đó là cách các ligature, cùng một số dạng tổ hợp, trở về thành các chữ cái cấu thành chúng.

  • Fonts: the hard part — bài đồng hành về phía diện mạo: nhúng, subset, và cách encoding được dựng. Trang này là phía ý nghĩa của cùng một đồng xu.
  • What makes a PDF accessible — văn bản trung thực là một thành phần; thứ tự đọc, các thẻ, và văn bản thay thế là phần còn lại.
  • What a PDF actually is — mô hình đối tượng mà các font, encoding và stream /ToUnicode sống bên trong.
  • Glyph — một hình thị giác trong một font (một đường viền). Thứ mà một PDF thực sự tô ra. Một glyph có một chỉ số nhưng không có ý nghĩa ký tự cố hữu.
  • Ký tự — một đơn vị của ngôn ngữ viết, được nhận diện bởi một điểm mã Unicode. Thứ bạn muốn nói, và thứ việc trích xuất cố gắng khôi phục.
  • — giá trị số trong một content stream chọn ra một glyph từ font hiện thời. Không phải một ký tự Unicode; chỉ có ý nghĩa tương đối với font.
  • CID — một định danh ký tự được một font tổng hợp dùng để đánh địa chỉ một glyph trong một tập lớn; một bước trung gian giữa mã và glyph.
  • Font tổng hợp (Type 0) — một font đọc các mã nhiều byte qua một CMap để đến các CID và glyph, được dùng khi một font phải đánh địa chỉ hàng nghìn glyph.
  • /ToUnicode — stream CMap ánh xạ các mã mà một tài liệu sử dụng ngược về các giá trị Unicode; thứ làm cho văn bản PDF tìm được, sao chép được và tiếp cận được.
  • Ligature — một glyph đơn vẽ hai chữ cái trở lên thành một hình (ví dụ fi); mục /ToUnicode của nó phân rã nó ngược về các ký tự của nó.