use super::super::*;
use super::common::*;
fn build_mcid_structure_tree_pdf() -> Vec<u8> {
let content = b"BT /F1 12 Tf\n\
/P <</MCID 0>> BDC 1 0 0 1 72 700 Tm (ALPHA) Tj EMC\n\
/P <</MCID 1>> BDC 1 0 0 1 72 600 Tm (BRAVO) Tj EMC\n\
/P <</MCID 2>> BDC 1 0 0 1 72 500 Tm (CHARLIE) Tj EMC\n\
ET\n";
let mut buf: Vec<u8> = Vec::new();
let mut off = vec![0usize; 9];
let obj = |buf: &mut Vec<u8>, off: &mut Vec<usize>, id: usize, body: &str| {
off[id] = buf.len();
buf.extend_from_slice(format!("{id} 0 obj\n{body}\nendobj\n").as_bytes());
};
let stream = |buf: &mut Vec<u8>, off: &mut Vec<usize>, id: usize, data: &[u8]| {
off[id] = buf.len();
buf.extend_from_slice(format!("{id} 0 obj\n<< /Length {} >>\nstream\n", data.len()).as_bytes());
buf.extend_from_slice(data);
buf.extend_from_slice(b"\nendstream\nendobj\n");
};
buf.extend_from_slice(b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n");
obj(
&mut buf,
&mut off,
1,
"<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot 7 0 R >>",
);
obj(&mut buf, &mut off, 2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
obj(
&mut buf,
&mut off,
3,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
/Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R /StructParents 0 >>",
);
stream(&mut buf, &mut off, 4, content);
obj(
&mut buf,
&mut off,
5,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
);
obj(&mut buf, &mut off, 7, "<< /Type /StructTreeRoot /K [8 0 R] >>");
obj(
&mut buf,
&mut off,
8,
"<< /Type /StructElem /S /Document /K [<< /Type /StructElem /S /P /Pg 3 0 R /K 0 >> \
<< /Type /StructElem /S /P /Pg 3 0 R /K 1 >> \
<< /Type /StructElem /S /P /Pg 3 0 R /K 2 >>] >>",
);
let xref = buf.len();
buf.extend_from_slice(b"xref\n0 9\n0000000000 65535 f \n");
for id in 1..=8 {
if id == 6 {
buf.extend_from_slice(b"0000000000 65535 f \n");
continue;
}
buf.extend_from_slice(format!("{:010} 00000 n \n", off[id]).as_bytes());
}
buf.extend_from_slice(b"trailer\n<< /Size 9 /Root 1 0 R >>\nstartxref\n");
buf.extend_from_slice(format!("{xref}\n%%EOF\n").as_bytes());
buf
}
#[test]
fn extra_span_with_borrowed_mcid_lands_in_structure_order() {
let doc = PdfDocument::from_bytes(build_mcid_structure_tree_pdf()).unwrap();
let plain = doc.extract_text(0).unwrap();
assert!(
plain.find("ALPHA") < plain.find("BRAVO") && plain.find("BRAVO") < plain.find("CHARLIE"),
"baseline structure order wrong: {plain:?}"
);
let extra = crate::layout::TextSpan {
text: "INSERTED".to_string(),
bbox: crate::geometry::Rect::new(72.0, 590.0, 50.0, 12.0),
font_size: 12.0,
mcid: Some(1),
..Default::default()
};
let opts = crate::converters::ConversionOptions {
extract_tables: true,
..Default::default()
};
let out = doc.extract_text_with_extra_spans(0, vec![extra], &opts).unwrap();
let (a, b, ins, c) = (
out.find("ALPHA"),
out.find("BRAVO"),
out.find("INSERTED"),
out.find("CHARLIE"),
);
assert!(ins.is_some(), "injected span dropped: {out:?}");
assert!(
a < b && b < ins && ins < c,
"injected span not placed in MCID-1 slot (expected ALPHA<BRAVO<INSERTED<CHARLIE): {out:?}"
);
}
#[test]
fn test_try_assemble_vertical_cjk_orders_columns_right_to_left() {
let mk = |t: &str, x: f32, y: f32| make_test_span(t, x, y, 18.0, 18.0);
let spans = vec![
mk("\u{4E00}", 116.0, 719.0),
mk("\u{4E8C}", 116.0, 701.0),
mk("\u{4E09}", 116.0, 683.0),
mk("\u{56DB}", 89.0, 719.0),
mk("\u{4E94}", 89.0, 701.0),
mk("\u{516D}", 89.0, 683.0),
mk("\u{4E03}", 62.0, 719.0),
mk("\u{516B}", 62.0, 701.0),
mk("\u{4E5D}", 62.0, 683.0),
];
assert_eq!(
PdfDocument::try_assemble_vertical_cjk(&spans).as_deref(),
Some("\u{4E00}\u{4E8C}\u{4E09}\u{56DB}\u{4E94}\u{516D}\u{4E03}\u{516B}\u{4E5D}")
);
}
#[test]
fn test_try_assemble_vertical_cjk_horizontal_returns_none() {
let mk = |t: &str, x: f32| make_test_span(t, x, 700.0, 18.0, 18.0);
let spans = vec![
mk("\u{4E00}", 62.0),
mk("\u{4E8C}", 80.0),
mk("\u{4E09}", 98.0),
mk("\u{56DB}", 116.0),
mk("\u{4E94}", 134.0),
mk("\u{516D}", 152.0),
mk("\u{4E03}", 170.0),
mk("\u{516B}", 188.0),
];
assert!(PdfDocument::try_assemble_vertical_cjk(&spans).is_none());
}
#[test]
fn test_try_assemble_vertical_cjk_multichar_runs_returns_none() {
let mk = |t: &str, y: f32| make_test_span(t, 60.0, y, 200.0, 18.0);
let spans = vec![
mk("標準マーケットモデルは", 700.0),
mk("次元で説明することは", 680.0),
mk("取引できない商品がマ", 660.0),
mk("クロ経済変数などである", 640.0),
mk("モデルを考えることは", 620.0),
mk("可能で非完備の場合は", 600.0),
mk("リスク中立確率は一意", 580.0),
mk("ではなく価格も一意でない", 560.0),
];
assert!(PdfDocument::try_assemble_vertical_cjk(&spans).is_none());
}
#[test]
fn test_try_assemble_vertical_cjk_latin_returns_none() {
let spans: Vec<TextSpan> = "the quick brown fox jumps over a lazy dog today"
.split(' ')
.enumerate()
.map(|(i, w)| make_test_span(w, 60.0 + i as f32 * 40.0, 700.0, 30.0, 12.0))
.collect();
assert!(PdfDocument::try_assemble_vertical_cjk(&spans).is_none());
}