doc_create from text refuses characters Helvetica can't draw (#959) (#1030)

doc_create with from=text wrote every character outside WinAnsiEncoding
(Korean, Chinese, Japanese, emoji) as `?` with no error. It now refuses
such text with the same message page_add_text already gives, naming the
first undrawable character and its code point. Line breaks, page breaks
and tabs are layout and still pass. create_from_text itself is unchanged,
so opening .txt files, comment summaries and compare reports behave as
before.
This commit is contained in:
Shukat Ibrahim authored and GitHub committed 2026-10-11 11:59:43 -04:00
1 parent 86e74ec121
commit 2fbce7028c
5 files changed
+39 -7

No files matched your search

+4
View File
@@ -1081,6 +1081,10 @@ impl Automation {
}
(None, None) => return Err(ToolError::InvalidArgs("text needs `text` or `path`".into())),
};
// The page font is Helvetica/WinAnsi: refuse what it would silently turn into `?`.
if let Some(c) = pdfcraft_engine::first_undrawable_plain(&text) {
return Err(ToolError::InvalidArgs(pdfcraft_engine::undrawable_message(c)));
}
(format!("{title}.pdf"), self.session.create_from_text(&title, &text).map_err(failed)?)
}
other => return Err(ToolError::InvalidArgs(format!("unknown source {other:?}"))),
+2 -2
View File
@@ -479,7 +479,7 @@ pub fn tools() -> Vec<ToolDef> {
t(
"page_add_text",
"Add text to a page",
"Add text as page content (not a comment). Place it with at [x, y] (top-left, points from the top-left of the displayed page) and width (wrap width, default 200), or rect. Newlines start new lines; long lines wrap. Style: font helvetica/times/courier, size, bold, italic, color (#RRGGBB or a name), align left/center/right. It stays editable with content_update. Undoable.",
"Add text as page content (not a comment). Place it with at [x, y] (top-left, points from the top-left of the displayed page) and width (wrap width, default 200), or rect. Newlines start new lines; long lines wrap. Style: font helvetica/times/courier, size, bold, italic, color (#RRGGBB or a name), align left/center/right. Text must be Western European (WinAnsi) characters; others are refused. It stays editable with content_update. Undoable.",
)
.cmd("edit.text")
.with(schema(
@@ -985,7 +985,7 @@ pub fn tools() -> Vec<ToolDef> {
t(
"doc_create",
"Create a PDF",
"Create a new, unsaved document and return it like doc_open: `blank` (pages, width, height in points; default 1 US Letter page), `images` (paths of PNG, JPEG, TIFF (every page), GIF or BMP files, one page each at the image's resolution) or `text` (a .txt path, or `text` directly). Save it with doc_save and a path.",
"Create a new, unsaved document and return it like doc_open: `blank` (pages, width, height in points; default 1 US Letter page), `images` (paths of PNG, JPEG, TIFF (every page), GIF or BMP files, one page each at the image's resolution) or `text` (a .txt path, or `text` directly; Western European (WinAnsi) characters only, others are refused). Save it with doc_save and a path.",
)
.with(schema(
json!({
+20
View File
@@ -1915,6 +1915,26 @@ fn creating_and_reducing_through_tools() {
assert!(matches!(a.call("doc_create", &json!({ "from": "images", "paths": ["notes.txt"] })), Err(ToolError::Failed(_))));
}
#[test]
fn creating_from_text_refuses_what_the_font_cannot_draw() {
let dir = workdir("create-undrawable");
std::fs::write(dir.join("kr.txt"), "ABC 제안서 시험").unwrap();
let mut a = auto(&dir);
let refused = |r: Result<Vec<Content>, ToolError>| match r {
Err(ToolError::InvalidArgs(m) | ToolError::Failed(m)) => assert!(m.contains("U+C81C"), "{m}"),
other => panic!("expected a refusal, got {other:?}"),
};
assert!(matches!(a.call("doc_create", &json!({ "from": "text", "text": "ABC 제안서 시험" })), Err(ToolError::InvalidArgs(_))));
refused(a.call("doc_create", &json!({ "from": "text", "text": "ABC 제안서 시험" })));
refused(a.call("doc_create", &json!({ "from": "text", "path": "kr.txt" })));
// WinAnsi characters, line breaks and tabs are still fine.
let d = ok(&mut a, "doc_create", json!({ "from": "text", "text": "Price: 5 € at café\r\n\tnext\u{c}page two" }))["doc"].as_u64().unwrap();
assert_eq!(page_text(&mut a, d).len(), 2);
// #959: page_add_text refuses it too.
let doc = ok(&mut a, "doc_create", json!({ "from": "blank" }))["doc"].as_u64().unwrap();
refused(a.call("page_add_text", &json!({ "doc": doc, "page": 1, "text": "ABC 제안서 시험", "at": [20, 20] })));
}
#[test]
fn creating_from_multiple_files_through_tools() {
let dir = workdir("create-multiple");
+12 -4
View File
@@ -387,10 +387,18 @@ pub(crate) fn drawable<'a>(lines: impl IntoIterator<Item = &'a str>) -> Result<(
/// The refusal for page text with `c`, which nothing here can draw.
pub(crate) fn undrawable(c: char) -> EditError {
EditError::Invalid(format!(
"the standard fonts can't draw \"{c}\" (U+{:04X}); only Western European characters can be added as text for now",
u32::from(c)
))
EditError::Invalid(undrawable_message(c))
}
/// The wording of the refusal for text with `c`, which the standard fonts can't draw.
pub fn undrawable_message(c: char) -> String {
format!("the standard fonts can't draw \"{c}\" (U+{:04X}); only Western European characters can be added as text for now", u32::from(c))
}
/// The first character of plain `text` that Helvetica/WinAnsi can't draw (line and page breaks
/// and tabs are layout, not glyphs).
pub fn first_undrawable_plain(text: &str) -> Option<char> {
text.chars().find(|c| !matches!(c, '\r' | '\n' | '\t' | '\u{c}') && !pdfcraft_fonts::win_ansi_encodable(*c))
}
fn text_op(x: f64, y: f64, text: &str) -> Vec<u8> {
+1 -1
View File
@@ -46,7 +46,7 @@ pub use pdfcraft_forms::{
};
pub use pdfcraft_a11y as a11y;
pub use pdfcraft_edit::{BlockStyle, PageImage, TextBlock, TextLine, first_undrawable};
pub use pdfcraft_edit::{BlockStyle, PageImage, TextBlock, TextLine, first_undrawable, first_undrawable_plain, undrawable_message};
pub use pdfcraft_measure as measure;
pub use pdfcraft_xfa::Report as XfaLayout;