mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M7: a bold paragraph's stand-in font no longer turns earlier edits bold (#4280)
Edit text set a paragraph whose font lacks a typed character (an accent missing from a subset) in a standard font, always under the resource name PCEdHelv, whatever face it stood for. Rewriting a bold paragraph after a regular one replaced PCEdHelv with Times-Bold, so every paragraph set in the earlier stand-in turned bold, and the editor's draft measurement saw those paragraphs change and grew its frame over them. The stand-in is now named after its face (PCEdTimesBold, ...), as the line and region rewrites already name theirs. Fixes #4280
This commit is contained in:
@@ -191,6 +191,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-23 (M7, #4280):** Edit text names each standard stand-in font after its face (`PCEdTimesBold`, ...) instead of one shared `PCEdHelv`. Rewriting a bold paragraph whose subset lacks an accent no longer repoints the paragraphs set earlier in a regular stand-in at Times-Bold, and the editor's frame stays on the paragraph being edited. Pages saved by older builds keep their shared name and are not repaired. Estimates unchanged.
|
||||
- **2026-10-22 (M4, #3916 partial):** Document edits and Undo/Redo cancel an Organize page drag. Pointer-release regressions check exact page order and save/reopen.
|
||||
- **2026-10-22 (M14, macOS association recovery candidate):** Successful desktop open/save/sign can queue recovery of an existing per-file binding to the running PDFCraft bundle through NSWorkspace. Metadata-only watcher changes retain undo and avoid false overwrite prompts; real same-length/same-mtime edits remain protected. Rebased on 0.12.0 and reuses #3042's shared atomic metadata preservation; the bounded recovery parser does not narrow save limits. Prior native Preview TEST-signature and Finder workflows passed on macOS 27.0.1; other versions remain unqualified. Estimates unchanged.
|
||||
- **2026-10-13 (M7, free image rotation):** A selected page image has a rotate knob (Shift: 15 degree steps) and `image_edit` a `rotate_by` action; the angle is written as one undoable `cm`. Snapping to page edges while moving is in #1740; resize snapping is not done. Estimates unchanged.
|
||||
|
||||
@@ -2414,6 +2414,44 @@ fn missing_glyphs_substitute_helvetica_and_impossible_text_is_refused() {
|
||||
assert!(text::replace_line(&mut doc, 0, 5, "x").is_err(), "no such line");
|
||||
}
|
||||
|
||||
/// #4280: two paragraphs in subsets without Ç or Ã, one regular and one bold. Each edit that adds
|
||||
/// an accent sets the paragraph in a standard font of its own weight. Editing the bold one must
|
||||
/// not turn the regular one, set in its stand-in earlier, bold too, and the editor's frame for the
|
||||
/// bold one must not reach the regular one.
|
||||
#[test]
|
||||
fn a_bold_stand_in_font_leaves_an_earlier_regular_one_alone() {
|
||||
let widths = vec!["600"; 26].join(" ");
|
||||
let font = |base: &str| {
|
||||
format!("<< /Type /Font /Subtype /Type1 /BaseFont /{base} /FirstChar 65 /LastChar 90 /Widths [{widths}] /Encoding /WinAnsiEncoding >>")
|
||||
};
|
||||
let (regular, bold) = (font("ABCDEF+Garamond"), font("ABCDEF+Garamond-Bold"));
|
||||
let content = stream("", "BT /F1 12 Tf 72 700 Td (ACAO) Tj ET BT /F2 12 Tf 300 400 Td (TENSAO) Tj ET");
|
||||
let mut doc = build(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
||||
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /Contents 4 0 R /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> >>",
|
||||
&content,
|
||||
®ular,
|
||||
&bold,
|
||||
]);
|
||||
let block = |doc: &Document, text: &str| text::text_blocks(doc, 0).unwrap().iter().position(|b| b.text == text).unwrap();
|
||||
let acao = block(&doc, "ACAO");
|
||||
assert_eq!(text::replace_block(&mut doc, 0, acao, "AÇÃO").unwrap().substituted.as_deref(), Some("Times-Roman"));
|
||||
let first = text::text_lines(&doc, 0).unwrap().into_iter().find(|l| l.text == "AÇÃO").unwrap();
|
||||
|
||||
let tensao = block(&doc, "TENSAO");
|
||||
let layout = text::text_edit_layout(&doc, 0, tensao, "TENSÃO", &text::BlockStyle::default()).unwrap();
|
||||
let draft = layout.draft_rect.unwrap();
|
||||
assert!(draft[3] < first.rect[1] || draft[1] > first.rect[3], "the frame {draft:?} reaches the regular paragraph at {:?}", first.rect);
|
||||
assert_eq!(text::replace_block(&mut doc, 0, tensao, "TENSÃO").unwrap().substituted.as_deref(), Some("Times-Bold"));
|
||||
|
||||
let doc = reopen(&doc);
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
let font_of = |text: &str| lines.iter().find(|l| l.text == text).map(|l| l.base_font.clone());
|
||||
assert_eq!(font_of("AÇÃO").as_deref(), Some("Times-Roman"), "{lines:?}");
|
||||
assert_eq!(font_of("TENSÃO").as_deref(), Some("Times-Bold"), "{lines:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraphs_are_found_and_rewrapped() {
|
||||
let mut doc = text_page(
|
||||
|
||||
@@ -1845,8 +1845,6 @@ fn source_family(base_font: &str) -> crate::added::Family {
|
||||
}
|
||||
}
|
||||
|
||||
const SUBSTITUTE_NAME: &str = "PCEdHelv";
|
||||
const SUBSTITUTE: &[u8] = SUBSTITUTE_NAME.as_bytes();
|
||||
const MAX_TYPE3_GLYPHS: usize = 240;
|
||||
|
||||
/// The generated fonts for one replacement: one per run of at most `max_glyphs` unique characters,
|
||||
@@ -2883,7 +2881,9 @@ fn rewrite_block_from(
|
||||
f.set(b"Subtype".to_vec(), Object::name("Type1"));
|
||||
f.set(b"BaseFont".to_vec(), Object::name(base));
|
||||
f.set(b"Encoding".to_vec(), Object::name("WinAnsiEncoding"));
|
||||
let name = if style.family.is_none() { String::from_utf8_lossy(SUBSTITUTE).into_owned() } else { format!("PCEd{}", base.replace('-', "")) };
|
||||
// Named after its face, as the line and region rewrites name theirs: one name for every
|
||||
// stand-in would repoint the paragraphs set in an earlier one at this face (#4280).
|
||||
let name = format!("PCEd{}", base.replace('-', ""));
|
||||
fonts_res.set(name.clone().into_bytes(), Object::Dict(f));
|
||||
if style.family.is_none() {
|
||||
substituted = Some(base.to_string());
|
||||
|
||||
@@ -1914,8 +1914,8 @@ milestone = "M7"
|
||||
status = "shipped"
|
||||
commands = ["edit.edit_text"]
|
||||
tools = ["text_edit"]
|
||||
evidence = ["crates/edit/src/tests.rs::missing_glyphs_substitute_helvetica_and_impossible_text_is_refused", "crates/fonts/src/pdf.rs::subset_fonts_only_encode_the_glyphs_they_have", "crates/fonts/src/pdf.rs::composite_fonts_use_to_unicode"]
|
||||
notes = "the line's font is reused when every character has a code and a glyph (ToUnicode, encodings, Differences; subset glyph checks); otherwise Helvetica (WinAnsi) is used and reported; text no font can show is refused"
|
||||
evidence = ["crates/edit/src/tests.rs::missing_glyphs_substitute_helvetica_and_impossible_text_is_refused", "crates/edit/src/tests.rs::a_bold_stand_in_font_leaves_an_earlier_regular_one_alone", "crates/fonts/src/pdf.rs::subset_fonts_only_encode_the_glyphs_they_have", "crates/fonts/src/pdf.rs::composite_fonts_use_to_unicode"]
|
||||
notes = "the line's font is reused when every character has a code and a glyph (ToUnicode, encodings, Differences; subset glyph checks); otherwise Helvetica (WinAnsi) is used and reported; text no font can show is refused. Each standard stand-in has a resource of its own face (PCEdTimesBold, ...), so a bold paragraph's stand-in no longer turns paragraphs set earlier in a regular one bold (#4280); pages already saved with the shared PCEdHelv name by older builds are not repaired"
|
||||
|
||||
[[feature]]
|
||||
id = "edit.text-format-font"
|
||||
|
||||
Reference in New Issue
Block a user