Repair a page's damaged THSarabunPSK map without grouped text

The repair reads the page's own fonts. It ran only after a group was expanded, so the same
damaged map on text that was never grouped kept reading wrong. Preparation now repairs it
whenever it finds it, recorded in the repair log as before, and PrepareText (text_prepare)
succeeds when it repaired a map even with nothing to expand. A repaired map no longer matches
the damage's fingerprint, so preparing again changes nothing.
This commit is contained in:
bflatastic committed 2026-11-08 22:31:14 +07:00
1 parent 90b88ddcb6
commit cc9f7977bf
5 files changed
+42 -12

No files matched your search

+3 -2
View File
@@ -229,8 +229,9 @@ summary it returns what this edit did:
| `repaired_fonts` | integer | Page fonts whose damaged legacy THSarabunPSK Thai map was repaired on a page-owned copy. |
| `refused_groups` | array of strings | One `name: reason` per group left as it was: unsafe compositing, tags, malformed content, or text grouped more than eight levels deep. |
It fails, changing nothing, when no group on the page can be prepared (the error gives the first
reason) or the page is out of range.
The page's own fonts with that damaged map are repaired too, whether their text was grouped or
not. It fails, changing nothing, when the page has nothing to prepare or repair (the error gives
the first reason a group was left as it was) or the page is out of range.
### Superscript and subscript normalization
+1 -1
View File
@@ -1591,7 +1591,7 @@ pub fn tools() -> Vec<ToolDef> {
t("text_fonts", "List document fonts", "List validated embedded source faces and approved full preview faces for a page. Full registered resources and bundled choices can be selected by text_edit.font_resource. builtin:thai explicitly replaces a paragraph or range with the bundled Noto Sans Thai; a build without that face never lists it and text_edit refuses it there (a page whose fonts fill the listing budget may also omit it, but text_edit still accepts it). preview_only faces cannot be selected.")
.ro().cmd("edit.edit_text")
.with(schema(json!({"doc":doc(), "page":{"type":"integer","minimum":1}}), &["doc","page"])),
t("text_prepare", "Prepare grouped text", "Make existing text inside supported Form XObjects directly editable on one page. Replaces placements with their original operators and resources, preserving position, clipping and other pages. Refuses unsafe transparency, tags or malformed content. Adds no text or cover boxes. Returns prepared_groups (placements made editable), repaired_fonts (damaged legacy Thai maps repaired) and refused_groups ('name: reason' for each group left as it was). Fails when nothing on the page can be prepared. Undoable; list text_paragraphs again afterwards.")
t("text_prepare", "Prepare grouped text", "Make existing text inside supported Form XObjects directly editable on one page. Replaces placements with their original operators and resources, preserving position, clipping and other pages. Refuses unsafe transparency, tags or malformed content. Adds no text or cover boxes. Returns prepared_groups (placements made editable), repaired_fonts (damaged legacy Thai maps repaired) and refused_groups ('name: reason' for each group left as it was). Also repairs the page's own damaged legacy Thai maps, grouped or not. Fails when the page has nothing to prepare or repair. Undoable; list text_paragraphs again afterwards.")
.cmd("edit.edit_text")
.with(schema(json!({"doc":doc(), "page":{"type":"integer","minimum":1}}), &["doc","page"])),
t("text_paragraphs", "List paragraphs", "The paragraphs on a page (adjacent source runs grouped into aligned visual rows, including mixed fonts, sizes and colours): number, text, editor_text (visual rows separated by newlines), rows and source line numbers, box (top-left-origin points), font and size. Use the number with text_edit's paragraph: until you list the page again, a number names the paragraph listed here, even after edits regroup the page.")
+24
View File
@@ -13023,6 +13023,30 @@ fn grouped_legacy_thai_map_repair_follows_every_block_of_the_damaged_map() {
assert!(lines.iter().any(|l| l.text == "ฮา"), "{:?}", lines.iter().map(|l| &l.text).collect::<Vec<_>>());
}
/// The damaged THSarabunPSK map of text that was never grouped is repaired by preparation too,
/// recorded in the repair log; with nothing to prepare or repair, preparation changes nothing.
#[test]
fn legacy_thai_map_repair_needs_no_grouped_text() {
let damaged = b"begincmap 1 begincodespacerange <0000> <ffff> endcodespacerange \
1 beginbfrange <00ff><01ff><0032> endbfrange 17 beginbfrange \
<00d9><00d9><0e04> <00e2><00e2><0e0d> <00fa><00fa><0e25> <0100><0100><0e2b> <0102><0102><0e2d> \
<0164><0164><0e4c> <0166><0166><0e31> <0168><0168><0e47> \
<00d6><00d6><000f> <00f0><00f0><0029> <00f8><00f8><0023> <0105><0105><003e> <0107><0107><0040> \
<0109><0109><003e> <0111><0111><004e> <0160><0160><004e> <0161><0161><004f> endbfrange endcmap";
// ฮา: codes 259 and 263, drawn by the page itself.
let mut doc = page_with_forms("BT /F1 14 Tf 40 500 Td <01030107> Tj ET", &[]);
let font = legacy_thai_font(&mut doc, "ABCDEF+THSarabunPSK", &legacy_thai_program(), damaged);
doc.set(pdfcraft_cos::ObjRef::new(5, 0), Object::Dict(font));
let result = text::forms::prepare(&mut doc, 0).unwrap();
assert_eq!((result.expanded, result.repaired_fonts), (0, 1));
assert!(text::text_lines(&doc, 0).unwrap().iter().any(|l| l.text == "ฮา"));
assert!(doc.repair_log().iter().any(|l| l.contains("THSarabunPSK")), "{:?}", doc.repair_log());
// Repaired once: the map no longer has the damage, and nothing else is to be done.
let before = pdfcraft_cos::write_full(&doc, &Default::default()).unwrap();
assert_eq!(text::forms::prepare(&mut doc, 0).unwrap().repaired_fonts, 0);
assert_eq!(pdfcraft_cos::write_full(&doc, &Default::default()).unwrap(), before);
}
/// A Thai run whose glyphs a subsetter split across two subsets of the face is kept in that
/// face: each cluster comes from a subset that has it. A cluster no subset has is refused with
/// the reason, and the document is unchanged.
+13 -8
View File
@@ -33,7 +33,8 @@ struct Inlined {
waiting: Vec<(usize, String)>,
}
/// Prepare grouped source text atomically, within eight levels and 64 placements per page.
/// Prepare grouped source text atomically, within eight levels and 64 placements per page, and
/// repair the page fonts' damaged legacy Thai maps (`repair_thai_fonts`), grouped or not.
/// Shared forms and other pages are untouched. Preparation is a distinct undoable engine edit.
pub fn prepare(doc: &mut Document, page: usize) -> Result<Preparation, EditError> {
let mut next = doc.clone();
@@ -179,16 +180,20 @@ pub fn prepare(doc: &mut Document, page: usize) -> Result<Preparation, EditError
}
result.refused.sort();
result.refused.dedup();
if result.expanded > 0 {
// The page's own damaged maps are repaired too, those of text that was never grouped
// included: its text reads wrong all the same.
result.repaired_fonts = repair_thai_fonts(&mut next, page)?;
if result.expanded > 0 || result.repaired_fonts > 0 {
for reason in &result.refused {
next.note_repair(format!("Grouped text left intact: {reason}"));
}
result.repaired_fonts = repair_thai_fonts(&mut next, page)?;
next.note_repair(format!(
"Prepared {} grouped text placement(s) on page {} for direct source-text editing",
result.expanded,
page.saturating_add(1)
));
if result.expanded > 0 {
next.note_repair(format!(
"Prepared {} grouped text placement(s) on page {} for direct source-text editing",
result.expanded,
page.saturating_add(1)
));
}
*doc = next;
}
Ok(result)
+1 -1
View File
@@ -734,7 +734,7 @@ pub(crate) fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut E
}
Edit::PrepareText { page } => {
let result = pdfcraft_edit::text::forms::prepare(doc, *page)?;
if result.expanded == 0 {
if result.expanded == 0 && result.repaired_fonts == 0 {
return Err(EditError::Invalid(
result.refused.first().cloned().unwrap_or_else(|| "this page has no supported grouped text to prepare".into()),
));