use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use rustyfi_backend::{
Closing, Color, FontKey, FontMetrics, GraphicsElem, HorzStringInfo, ImageId, ImageResource,
Length, Page, PageGeometry, Path as GrPath, PathSeg, PlacedLine, PureHorzBox, Subpath,
};
fn rectangle_path() -> GrPath {
GrPath {
subpaths: vec![Subpath {
start: (Length::pt(0.0), Length::pt(0.0)),
segs: vec![
PathSeg::Line((Length::pt(20.0), Length::pt(0.0))),
PathSeg::Line((Length::pt(20.0), Length::pt(20.0))),
PathSeg::Line((Length::pt(0.0), Length::pt(20.0))),
],
closing: Closing::Line,
}],
}
}
fn page_with_fill_then_text() -> Page {
let path = rectangle_path();
let text_info = HorzStringInfo { font: FontKey(0), size: Length::pt(10.0), rising: Length::ZERO, color: Color::Gray(0.0) };
let elems = vec![
GraphicsElem::Fill(Color::Rgb(1.0, 0.0, 0.0), path),
GraphicsElem::Text {
pt: (Length::pt(5.0), Length::pt(10.0)),
contents: vec![(
Length::ZERO,
PureHorzBox::InnerString {
info: text_info,
text: "Hi".to_string(),
width: Length::pt(10.0),
height: Length::pt(7.5),
depth: Length::pt(2.5),
},
)],
width: Length::pt(10.0),
height: Length::pt(7.5),
depth: Length::pt(2.5),
transform: None,
},
];
let gbox = PureHorzBox::Graphics {
width: Length::pt(20.0),
height: Length::pt(20.0),
depth: Length::pt(0.0),
elems,
origin_independent: false,
};
Page {
body_lines: usize::MAX,
lines: vec![PlacedLine {
x: Length::pt(50.0),
baseline_y: Length::pt(100.0),
contents: vec![(Length::ZERO, gbox)],
}],
}
}
fn round_geometry() -> PageGeometry {
PageGeometry {
paper_width: Length::pt(200.0),
paper_height: Length::pt(300.0),
text_origin: (Length::pt(20.0), Length::pt(20.0)),
text_width: Length::pt(160.0),
text_height: Length::pt(260.0),
}
}
fn span_after<'a>(hay: &'a str, needle: &str) -> &'a str {
let start = hay.find(needle).unwrap_or_else(|| panic!("missing {needle:?} in:\n{hay}"));
&hay[start..]
}
#[test]
fn base14_draw_text_emits_td_tj_inside_the_boxs_q_cm_q() {
let geometry = round_geometry();
let page = page_with_fill_then_text();
let bytes = rustyfi_pdf::render_pdf(&geometry, std::slice::from_ref(&page), &[])
.expect("PDF rendering must succeed");
assert!(bytes.starts_with(b"%PDF-"), "not a PDF header");
let hay = String::from_utf8_lossy(&bytes);
let expected_ty = geometry.paper_height.0 - 100.0;
let expected_cm = format!("1 0 0 1 50 {expected_ty} cm");
assert!(
hay.contains(&expected_cm),
"content stream missing the box's placement transform {expected_cm:?}:\n{hay}"
);
let after_cm = span_after(&hay, &expected_cm);
let fill_pos = after_cm.find("f*").expect("fill op");
let bt_pos = after_cm.find("BT").expect("BT (text run)");
assert!(fill_pos < bt_pos, "expected the fill before the text run (z-order)");
for op in ["BT", "10 Tf", "5 10 Td", "(Hi) Tj", "ET"] {
assert!(
after_cm.contains(op),
"content stream (after the box's cm) missing {op:?}:\n{after_cm}"
);
}
let after_text = &after_cm[after_cm.find("ET").unwrap()..];
assert!(after_text.contains('Q'), "expected the box to still close with Q after its text");
}
#[test]
fn base14_draw_text_on_an_empty_run_emits_no_text_operators() {
let geometry = round_geometry();
let elems = vec![GraphicsElem::Text {
pt: (Length::pt(1.0), Length::pt(2.0)),
contents: Vec::new(),
width: Length::ZERO,
height: Length::ZERO,
depth: Length::ZERO,
transform: None,
}];
let gbox = PureHorzBox::Graphics {
width: Length::ZERO,
height: Length::ZERO,
depth: Length::ZERO,
elems,
origin_independent: false,
};
let page = Page {
body_lines: usize::MAX,
lines: vec![PlacedLine {
x: Length::ZERO,
baseline_y: Length::ZERO,
contents: vec![(Length::ZERO, gbox)],
}],
};
let bytes = rustyfi_pdf::render_pdf(&geometry, std::slice::from_ref(&page), &[])
.expect("PDF rendering must succeed");
let hay = String::from_utf8_lossy(&bytes);
assert!(!hay.contains("BT"), "empty Text run should emit no BT:\n{hay}");
}
fn find_regular_font() -> Option<PathBuf> {
if let Ok(output) = Command::new("fc-match").args(["--format=%{file}", "DejaVuSans"]).output() {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() && Path::new(&path).is_file() {
return Some(PathBuf::from(path));
}
}
}
for candidate in [
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
"/run/current-system/sw/share/fonts/truetype/DejaVuSans.ttf",
"/run/current-system/sw/share/X11/fonts/DejaVuSans.ttf",
] {
if Path::new(candidate).is_file() {
return Some(PathBuf::from(candidate));
}
}
None
}
#[test]
fn cid_draw_text_run_survives_pdftotext_extraction() {
let path = match find_regular_font() {
Some(p) => p,
None => {
eprintln!(
"skipping: no DejaVuSans-like TrueType font found on this system \
(tried `fc-match DejaVuSans` and common nix/distro paths)"
);
return;
}
};
let store = rustyfi_pdf::TtfFontStore::load(&path, None, None).expect("load font");
let geometry = round_geometry();
let font = FontKey(0);
let size = Length::pt(18.0);
let text = "Hi";
let width = store.text_width(font, text, size).expect("glyphs exist");
let ascender = store.ascender(font, size);
let descender = store.descender(font, size);
let elems = vec![GraphicsElem::Text {
pt: (Length::pt(0.0), Length::pt(0.0)),
contents: vec![(
Length::ZERO,
PureHorzBox::InnerString {
info: HorzStringInfo { font, size, rising: Length::ZERO, color: Color::Gray(0.0) },
text: text.to_string(),
width,
height: ascender,
depth: descender,
},
)],
width,
height: ascender,
depth: descender,
transform: None,
}];
let gbox = PureHorzBox::Graphics {
width,
height: ascender,
depth: descender,
elems,
origin_independent: false,
};
let page = Page {
body_lines: usize::MAX,
lines: vec![PlacedLine {
x: geometry.text_origin.0,
baseline_y: geometry.text_origin.1 + ascender,
contents: vec![(Length::ZERO, gbox)],
}],
};
let pdf_bytes =
rustyfi_pdf::render_pdf_ttf(&geometry, &[page], &store, &[]).expect("render");
assert!(pdf_bytes.starts_with(b"%PDF-"));
let Ok(which) = Command::new("which").arg("pdftotext").output() else {
eprintln!("skipping pdftotext check: `which` not available");
return;
};
if !which.status.success() {
eprintln!("skipping pdftotext check: pdftotext not on PATH");
return;
}
let mut tmp = std::env::temp_dir();
tmp.push(format!("rustyfi-pdf-draw-text-cid-{}.pdf", std::process::id()));
std::fs::File::create(&tmp)
.and_then(|mut f| f.write_all(&pdf_bytes))
.expect("write temp pdf");
let output = Command::new("pdftotext").arg(&tmp).arg("-").output().expect("run pdftotext");
let _ = std::fs::remove_file(&tmp);
assert!(output.status.success(), "pdftotext failed: {output:?}");
let extracted = String::from_utf8_lossy(&output.stdout);
assert!(
extracted.contains("Hi"),
"pdftotext output missing graphics-positioned text \"Hi\", got: {extracted:?}"
);
}
#[test]
fn a_text_free_graphics_box_is_unaffected_by_the_nested_emitter_plumbing() {
let geometry = round_geometry();
let path = rectangle_path();
let elems = vec![GraphicsElem::Fill(Color::Rgb(1.0, 0.0, 0.0), path)];
let gbox = PureHorzBox::Graphics {
width: Length::pt(20.0),
height: Length::pt(20.0),
depth: Length::pt(0.0),
elems,
origin_independent: false,
};
let page = Page {
body_lines: usize::MAX,
lines: vec![PlacedLine {
x: Length::pt(50.0),
baseline_y: Length::pt(100.0),
contents: vec![(Length::ZERO, gbox)],
}],
};
let bytes = rustyfi_pdf::render_pdf(&geometry, std::slice::from_ref(&page), &[])
.expect("PDF rendering must succeed");
let hay = String::from_utf8_lossy(&bytes);
assert!(!hay.contains("BT"), "no Text element present, so no BT should appear:\n{hay}");
assert!(hay.contains("f*"));
}
#[test]
fn an_image_nested_inside_a_draw_text_run_gets_its_xobject_registered() {
let geometry = round_geometry();
let image = ImageResource {
samples: vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0],
px_w: 2,
px_h: 2,
jpeg_dct: None,
pdf: None,
};
let elems = vec![GraphicsElem::Text {
pt: (Length::pt(0.0), Length::pt(0.0)),
contents: vec![(
Length::ZERO,
PureHorzBox::Image {
width: Length::pt(10.0),
height: Length::pt(10.0),
image: ImageId(0),
},
)],
width: Length::pt(10.0),
height: Length::pt(10.0),
depth: Length::ZERO,
transform: None,
}];
let gbox = PureHorzBox::Graphics {
width: Length::pt(10.0),
height: Length::pt(10.0),
depth: Length::ZERO,
elems,
origin_independent: false,
};
let page = Page {
body_lines: usize::MAX,
lines: vec![PlacedLine {
x: Length::ZERO,
baseline_y: Length::ZERO,
contents: vec![(Length::ZERO, gbox)],
}],
};
let bytes = rustyfi_pdf::render_pdf(&geometry, std::slice::from_ref(&page), &[image])
.expect("PDF rendering must succeed");
let hay = String::from_utf8_lossy(&bytes);
assert!(hay.contains("/Im0"), "expected the image's XObject to be registered:\n{hay}");
assert!(hay.contains("Do"), "expected a Do operator placing the image:\n{hay}");
}