mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 12:38:58 -06:00
Integrate KenM76 PR #4906
This commit is contained in:
@@ -188,7 +188,10 @@ pub(crate) fn mcid_texts(doc: &Document, page: &Dict) -> HashMap<i64, String> {
|
||||
// Where each id's last glyph ended, in user space.
|
||||
let mut ends: HashMap<i64, (f64, f64)> = HashMap::new();
|
||||
let mut gs = TextState::default();
|
||||
let mut saved: Vec<TextState> = Vec::new();
|
||||
// With each saved state, the text and text line matrices: inside a text object `q`/`Q`
|
||||
// save and restore them too (ISO 32000-2 §9.4.1, as corrected by the PDF Association's
|
||||
// errata, issue 368), as the renderer does.
|
||||
let mut saved: Vec<(TextState, Matrix, Matrix)> = Vec::new();
|
||||
let mut unsaved = 0usize;
|
||||
let (mut tm, mut tlm) = (Matrix::IDENTITY, Matrix::IDENTITY);
|
||||
for op in pdfcraft_content::parse(&page_content(doc, page)).ops {
|
||||
@@ -209,12 +212,12 @@ pub(crate) fn mcid_texts(doc: &Document, page: &Dict) -> HashMap<i64, String> {
|
||||
b"EMC" => {
|
||||
stack.pop();
|
||||
}
|
||||
b"q" if saved.len() < MAX_SAVED => saved.push(gs.clone()),
|
||||
b"q" if saved.len() < MAX_SAVED => saved.push((gs.clone(), tm, tlm)),
|
||||
b"q" => unsaved += 1,
|
||||
b"Q" if unsaved > 0 => unsaved -= 1,
|
||||
b"Q" => {
|
||||
if let Some(saved) = saved.pop() {
|
||||
gs = saved;
|
||||
(gs, tm, tlm) = saved;
|
||||
}
|
||||
}
|
||||
b"cm" => {
|
||||
|
||||
@@ -1235,6 +1235,19 @@ fn headings_drawn_glyph_by_glyph_keep_their_words() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Inside a text object `q`/`Q` save and restore the text matrices (ISO 32000-2 §9.4.1, as
|
||||
/// corrected by the PDF Association's errata, issue 368), as the renderer does: a word whose
|
||||
/// glyphs continue after a `Q` stays one word.
|
||||
#[test]
|
||||
fn q_inside_a_text_object_restores_the_text_matrices() {
|
||||
let doc = heading_page(&[
|
||||
"BT /F1 20 Tf 72 700 Td (Chap) Tj q 0 -100 Td Q (ter One) Tj ET".into(),
|
||||
"BT /F1 20 Tf 72 650 Td (Chap) Tj q 1 0 0 1 300 300 Tm Q (ter Two) Tj ET".into(),
|
||||
]);
|
||||
let titles: Vec<String> = headings(&doc).into_iter().map(|h| h.title).collect();
|
||||
assert_eq!(titles, ["Chapter One", "Chapter Two"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_untagged_document_has_no_headings() {
|
||||
let mut objs = headings_doc();
|
||||
|
||||
@@ -7536,3 +7536,24 @@ fn a_run_of_spaces_in_a_subset_without_a_space_glyph_keeps_its_width() {
|
||||
let content = String::from_utf8_lossy(&page_content_bytes(&reopen(&doc), 0)).into_owned();
|
||||
assert_eq!(content.matches("[-330] TJ").count(), 5, "{content}");
|
||||
}
|
||||
|
||||
/// Inside a text object `q`/`Q` save and restore the text and text line matrices (ISO 32000-2
|
||||
/// §9.4.1, as corrected by the PDF Association's errata, issue 368), as the renderer does: the
|
||||
/// text after `Q` is found, and stays, where it is drawn.
|
||||
#[test]
|
||||
fn q_inside_a_text_object_restores_the_text_matrices() {
|
||||
for (content, text, baseline) in [
|
||||
("BT /F1 12 Tf 72 700 Td q 0 -100 Td Q (After) Tj ET", "After", [72.0, 700.0]),
|
||||
("BT /F1 12 Tf 72 700 Td q 1 0 0 1 300 300 Tm Q (After) Tj ET", "After", [72.0, 700.0]),
|
||||
// `T*` moves from the restored line, not the one set inside q/Q.
|
||||
("BT /F1 12 Tf 14 TL 72 700 Td q 0 -100 Td Q T* (Next) Tj ET", "Next", [72.0, 686.0]),
|
||||
] {
|
||||
let mut doc = text_page(content);
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
assert_eq!((lines[0].text.as_str(), lines[0].baseline()), (text, baseline), "{content}");
|
||||
text::replace_line(&mut doc, 0, 0, "Edited").unwrap();
|
||||
let doc = reopen(&doc);
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
assert_eq!((lines[0].text.as_str(), lines[0].baseline()), ("Edited", baseline), "{content} after editing");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +425,10 @@ fn positioned_tracking(shown: &mut [Shown]) {
|
||||
/// stream), an unbalanced `q`, the font and the colour still apply in the streams after it.
|
||||
pub(super) struct Carry {
|
||||
pub(super) ts: Ts,
|
||||
pub(super) stack: Vec<Ts>,
|
||||
/// Each saved state with the text and text line matrices: inside a text object `q`/`Q`
|
||||
/// save and restore them too (ISO 32000-2 §9.4.1, as corrected by the PDF Association's
|
||||
/// errata, issue 368), as the renderer does.
|
||||
pub(super) stack: Vec<(Ts, Matrix, Matrix)>,
|
||||
pub(super) marked: Vec<Option<usize>>,
|
||||
/// For each open marked-content sequence, its optional content group when it is `/OC`.
|
||||
pub(super) layers: Vec<Option<Vec<u8>>>,
|
||||
@@ -761,7 +764,7 @@ pub(super) fn interpret(
|
||||
let mut out: Vec<Shown> = Vec::new();
|
||||
let mut ts = carry.ts.clone();
|
||||
let mut at_bt = (0usize, ts.clone());
|
||||
let mut stack: Vec<Ts> = std::mem::take(&mut carry.stack);
|
||||
let mut stack: Vec<(Ts, Matrix, Matrix)> = std::mem::take(&mut carry.stack);
|
||||
let (mut tm, mut tlm) = (Matrix::IDENTITY, Matrix::IDENTITY);
|
||||
let mut bt = 0usize;
|
||||
let mut marked =
|
||||
@@ -776,10 +779,10 @@ pub(super) fn interpret(
|
||||
b"EMC" => {
|
||||
marked.end(i, &mut out);
|
||||
}
|
||||
b"q" => stack.push(ts.clone()),
|
||||
b"q" => stack.push((ts.clone(), tm, tlm)),
|
||||
b"Q" => {
|
||||
if let Some(s) = stack.pop() {
|
||||
ts = s;
|
||||
if let Some(saved) = stack.pop() {
|
||||
(ts, tm, tlm) = saved;
|
||||
}
|
||||
}
|
||||
b"cm" => {
|
||||
|
||||
Reference in New Issue
Block a user