mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 02:58:32 -06:00
fix(text): read a TrueType font without an encoding as WinAnsi (#1477)
* fix(text): read a TrueType font without an encoding as WinAnsi (fixes #1468) * fix(text): describe the WinAnsi reading by pdftotext's output --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
0dd5fdd5bd
commit
685fd7eeea
@@ -2534,4 +2534,43 @@ mod tests {
|
||||
let all = t.glyphs_in([0.0, 0.0, 60.0, 40.0]);
|
||||
assert_eq!(t.column_text(&all), "日本\n語文");
|
||||
}
|
||||
|
||||
/// An embedded TrueType font with no `/Encoding`, flagged `flags`, its `/ToUnicode` (if
|
||||
/// any) mapping only printable ASCII, as OCR engines write the text layer of a scan.
|
||||
fn ocr_layer_text(flags: i64, ascii_to_unicode: bool) -> String {
|
||||
use lopdf::{Document, Object, Stream, dictionary};
|
||||
let mut doc = Document::with_version("1.7");
|
||||
let program = doc.add_object(Stream::new(dictionary! {}, include_bytes!("../../../assets/fonts/Inter-Regular.ttf").to_vec()));
|
||||
let descriptor =
|
||||
doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "Inter-Regular", "Flags" => flags, "FontFile2" => program });
|
||||
let mut font = dictionary! { "Type" => "Font", "Subtype" => "TrueType", "BaseFont" => "Inter-Regular", "FontDescriptor" => descriptor };
|
||||
if ascii_to_unicode {
|
||||
let cmap = b"/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Ascii def /CMapType 2 def 1 begincodespacerange <00> <FF> endcodespacerange 1 beginbfrange <20> <7E> <0020> endbfrange endcmap CMapName currentdict /CMap defineresource pop end end";
|
||||
font.set("ToUnicode", doc.add_object(Stream::new(dictionary! {}, cmap.to_vec())));
|
||||
}
|
||||
let font = doc.add_object(font);
|
||||
// WinAnsi bytes: "Rückfragen für Überhitzungsschutz: können, weiß, Café".
|
||||
let mut ops = b"BT /F1 14 Tf 1 0 0 1 20 200 Tm (".to_vec();
|
||||
ops.extend_from_slice(b"R\xfcckfragen f\xfcr \xdcberhitzungsschutz: k\xf6nnen, wei\xdf, Caf\xe9");
|
||||
ops.extend_from_slice(b") Tj ET");
|
||||
let content = doc.add_object(Stream::new(dictionary! {}, ops));
|
||||
let pages = doc.new_object_id();
|
||||
let page = doc.add_object(dictionary! { "Type" => "Page", "Parent" => pages, "MediaBox" => vec![Object::Integer(0), Object::Integer(0), Object::Integer(400), Object::Integer(400)], "Resources" => dictionary! {"Font" => dictionary! {"F1" => font}}, "Contents" => content });
|
||||
doc.objects.insert(pages, dictionary! {"Type" => "Pages", "Kids" => vec![Object::Reference(page)], "Count" => 1}.into());
|
||||
let catalog = doc.add_object(dictionary! {"Type" => "Catalog", "Pages" => pages});
|
||||
doc.trailer.set("Root", catalog);
|
||||
let mut bytes = Vec::new();
|
||||
doc.save_to(&mut bytes).unwrap();
|
||||
let pdf = Pdf::new(std::sync::Arc::new(bytes)).unwrap();
|
||||
extract_page(&pdf, 0, &InterpreterSettings::default()).unwrap().plain_text()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truetype_without_an_encoding_reads_its_codes_as_win_ansi() {
|
||||
// Café: 0xE9 is /Oslash in StandardEncoding, so reading the codes as Standard first and
|
||||
// WinAnsi only for the gaps would still give "CafØ".
|
||||
let expected = "Rückfragen für Überhitzungsschutz: können, weiß, Café";
|
||||
assert_eq!(ocr_layer_text(4, false), expected, "symbolic, no /ToUnicode");
|
||||
assert_eq!(ocr_layer_text(32, true), expected, "nonsymbolic, /ToUnicode for ASCII only");
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+15
-2
@@ -1,5 +1,5 @@
|
||||
use crate::font::blob::{CffFontBlob, OpenTypeFontBlob};
|
||||
use crate::font::generated::{glyph_names, mac_os_roman, mac_roman, standard};
|
||||
use crate::font::generated::{glyph_names, mac_os_roman, mac_roman, standard, win_ansi};
|
||||
use crate::font::standard_font::StandardKind;
|
||||
use crate::font::{
|
||||
Encoding, FallbackFontQuery, FontFlags, glyph_name_to_unicode, read_to_unicode,
|
||||
@@ -191,7 +191,7 @@ impl TrueTypeFont {
|
||||
|
||||
match &self.kind {
|
||||
Kind::Embedded(e) => e
|
||||
.code_to_name(code as u8)
|
||||
.code_to_unicode_name(code as u8)
|
||||
.and_then(glyph_name_to_unicode)
|
||||
.map(BfString::Char),
|
||||
Kind::Standard(s) => s.char_code_to_unicode(code as u8).map(BfString::Char),
|
||||
@@ -277,6 +277,19 @@ impl EmbeddedKind {
|
||||
.or_else(|| standard::get(code))
|
||||
}
|
||||
|
||||
/// PdfCraft patch: the glyph name text extraction reads a code as. Without a base encoding
|
||||
/// a TrueType font's codes are read as WinAnsi, matching pdftotext's output, not as
|
||||
/// Standard: OCR engines write the text layer of a scan with WinAnsi codes and no
|
||||
/// `/Encoding`, and Standard has no name for ü, ö or ß and gives é (0xE9) as Ø. Glyph
|
||||
/// selection (`code_to_name`) is unchanged.
|
||||
fn code_to_unicode_name(&self, code: u8) -> Option<&str> {
|
||||
self.differences
|
||||
.get(&code)
|
||||
.map(|s| s.as_str())
|
||||
.or_else(|| self.encoding.map_code(code))
|
||||
.or_else(|| win_ansi::get(code))
|
||||
}
|
||||
|
||||
fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
|
||||
self.base_font.outline_glyph(glyph)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user