PDF import: say when invisible text is left out (part of #2675)

Text in rendering mode 3 (invisible: a scanned page's searchable OCR layer) was dropped with no
warning, so a scanned PDF opened and saved again lost its searchable text without a hint. The
import now reports INVISIBLE_TEXT_NOTE when it leaves such text out, for type and outlines alike.

Keeping the text needs a kind of type that is kept but not drawn, and the PDF writer (krilla, in
craft-libs) to write it with `3 Tr`; its text API has fill, stroke and fill-and-stroke only.
This commit is contained in:
00xJ0
2026-10-19 22:19:50 +07:00
committed by bflatastic
parent 4614c08476
commit 4aa4b0e70b
3 changed files with 28 additions and 3 deletions
+8 -1
View File
@@ -149,6 +149,10 @@ pub const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it,
/// Warning: glyphs were kept as outlines rather than read as type.
pub const OUTLINED_TEXT_NOTE: &str = "text was converted to outlines";
/// Warning: text drawn invisibly (text rendering mode 3, as a scanned page's OCR layer is) was left
/// out: VectorCraft has no type that is kept but not drawn, so a PDF saved from it loses that
/// searchable text (#2675).
pub const INVISIBLE_TEXT_NOTE: &str = "invisible text (such as a scanned page's searchable OCR layer) was left out";
/// Warning: glyphs that differ from the installed font of their name were kept as outlines.
pub const UNMATCHED_GLYPHS_NOTE: &str = "text whose glyphs differ from the installed font of the same name was kept as outlines";
/// The end of the warning that glyphs whose characters the file doesn't name were kept as
@@ -1963,7 +1967,10 @@ impl<'a> Device<'a> for Builder<'_> {
) {
self.pending = false;
let stroke = match draw_mode {
GlyphDrawMode::Invisible => return,
GlyphDrawMode::Invisible => {
self.warn(INVISIBLE_TEXT_NOTE);
return;
}
GlyphDrawMode::Fill => None,
GlyphDrawMode::Stroke(p) => Some(p.clone()),
};
+2 -2
View File
@@ -74,8 +74,8 @@ pub use encrypt::Encryption;
pub use export::{export, export_with_report, page_areas};
pub use file_info::{FileInfo, file_info};
pub use import::{
OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines, import,
import_with_report, is_glyph_outlines, is_outlines_note,
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines,
import, import_with_report, is_glyph_outlines, is_outlines_note,
};
pub use pages::{PageInfo, PdfInfo, has_illustrator_data, illustrator_data, info, is_postscript};
pub use post::{THUMBNAIL_SIZE, Thumbnail};
+18
View File
@@ -177,3 +177,21 @@ pub(crate) fn transfer_contract() {
fn soft_mask_inversion_uses_quantized_transfer_endpoints() {
transfer_contract();
}
/// #2675: text in rendering mode 3 (invisible, as a scanned page's OCR layer) isn't kept, and the
/// import now says so, for type and outlines alike. Visible text beside it still comes in, and a
/// page without invisible text gets no such warning.
#[test]
fn invisible_text_is_reported_when_left_out() {
let open = |content: &str, text_as: TextAs| import_with_report(&text_pdf(content), &ImportOptions { text_as, ..Default::default() }).unwrap();
let invisible = "BT /F1 20 Tf 3 Tr 1 0 0 1 10 40 Tm (AB) Tj 0 Tr 1 0 0 1 10 10 Tm (CA) Tj ET";
for text_as in [TextAs::Text, TextAs::Outlines] {
let r = open(invisible, text_as);
assert!(r.warnings.iter().any(|w| w == crate::INVISIBLE_TEXT_NOTE), "{text_as:?}: {:?}", r.warnings);
let mut kept = 0;
r.document.walk(|n| kept += usize::from(matches!(n.kind, NodeKind::Text(_) | NodeKind::Path { .. } | NodeKind::Compound { .. })));
assert!(kept >= 1, "{text_as:?}: the visible line still comes in");
let visible = open("BT /F1 20 Tf 1 0 0 1 10 10 Tm (CA) Tj ET", text_as);
assert!(!visible.warnings.iter().any(|w| w == crate::INVISIBLE_TEXT_NOTE), "{text_as:?}: {:?}", visible.warnings);
}
}