use std::path::Path;
use std::rc::Rc;
use rustyfi_backend::{FontKey, FontMetrics, Length};
use rustyfi_lang::value::DocumentValue;
struct Mono;
impl FontMetrics for Mono {
fn advance(&self, _f: FontKey, c: char, size: Length) -> Option<Length> {
if c.is_ascii() {
Some(size * 0.5)
} else {
None
}
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size * 0.75
}
fn descender(&self, _f: FontKey, size: Length) -> Length {
size * 0.25
}
}
fn fixture_path() -> String {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/dot.png")
.to_str()
.expect("fixture path must be valid UTF-8")
.to_string()
}
fn jpeg_fixture_path() -> String {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/dot.jpg")
.to_str()
.expect("fixture path must be valid UTF-8")
.to_string()
}
fn compile_document_with_stdlib(src: &str) -> Rc<DocumentValue> {
let lib_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../lib-rustyfi/dist/packages/stdja-mini.satyh");
let lib_src = std::fs::read_to_string(&lib_path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", lib_path.display()));
let lib_file = rustyfi_syntax::parse_file(&lib_src).expect("stdlib must parse");
let doc_file = rustyfi_syntax::parse_file(src).expect("fixture source must parse");
let mut prelude = lib_file.prelude;
prelude.extend(doc_file.prelude);
let merged = rustyfi_syntax::cst::File {
headers: Vec::new(),
prelude,
in_kw: doc_file.in_kw,
body: doc_file.body,
eoi: doc_file.eoi,
};
rustyfi_lang::compile_document_cst(&merged, &Mono).expect("document must compile")
}
#[test]
fn image_in_a_paragraph_renders_as_a_pdf_image_xobject() {
let src = "let-inline ctx \\fig it = use-image-by-width (load-image `__FIXTURE__`) 40pt
in
document (||) '< +p { here: \\fig{ignored} done } >"
.replace("__FIXTURE__", &fixture_path());
let doc = compile_document_with_stdlib(&src);
assert_eq!(doc.pages.len(), 1, "expected a single page");
assert_eq!(
doc.images.len(),
1,
"load-image should have decoded exactly one image into DocumentValue::images"
);
let has_image_box = doc.pages[0].lines.iter().any(|line| {
line.contents
.iter()
.any(|(_, bx)| matches!(bx, rustyfi_backend::PureHorzBox::Image { .. }))
});
assert!(has_image_box, "expected an Image box on the placed line");
let bytes = rustyfi_pdf::render_pdf(&doc.geometry, &doc.pages, &doc.images)
.expect("render_pdf must succeed with an Image box present");
assert!(
bytes.starts_with(b"%PDF-"),
"output must start with a PDF header"
);
let text = String::from_utf8_lossy(&bytes);
assert!(
text.contains("/Subtype /Image"),
"expected an Image XObject (/Subtype /Image): {text}"
);
assert!(
text.contains("/XObject"),
"expected an /XObject resource entry: {text}"
);
assert!(
text.contains(" Do"),
"expected a content-stream `Do` (x_object) operator: {text}"
);
assert!(text.contains("/DeviceRGB"), "expected a DeviceRGB color space: {text}");
assert!(
text.contains("/BitsPerComponent 8"),
"expected 8-bit samples: {text}"
);
}
#[test]
fn text_only_document_has_no_xobject_and_is_unaffected_by_the_images_parameter() {
let doc = compile_document_with_stdlib("document (||) '< +p { hello world } >");
assert!(doc.images.is_empty());
let bytes = rustyfi_pdf::render_pdf(&doc.geometry, &doc.pages, &doc.images).unwrap();
let text = String::from_utf8_lossy(&bytes);
assert!(!text.contains("/XObject"), "no image was ever placed: {text}");
assert!(!text.contains("/Subtype /Image"));
}
#[test]
fn jpeg_image_embeds_via_dctdecode_passthrough_not_a_flate_reencode() {
let src = "let-inline ctx \\fig it = use-image-by-width (load-image `__FIXTURE__`) 40pt
in
document (||) '< +p { here: \\fig{ignored} done } >"
.replace("__FIXTURE__", &jpeg_fixture_path());
let doc = compile_document_with_stdlib(&src);
assert_eq!(doc.images.len(), 1);
assert_eq!(doc.images[0].px_w, 8);
assert_eq!(doc.images[0].px_h, 4);
let dct = doc.images[0]
.jpeg_dct
.as_ref()
.expect("a baseline JPEG source must record jpeg_dct");
assert_eq!(dct.components, 3, "dot.jpg is a 3-component YCbCr/RGB JPEG");
let bytes = rustyfi_pdf::render_pdf(&doc.geometry, &doc.pages, &doc.images)
.expect("render_pdf must succeed with a JPEG Image box present");
assert!(bytes.starts_with(b"%PDF-"));
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("/Subtype /Image"));
assert!(text.contains("/XObject"));
assert!(text.contains(" Do"));
assert!(
text.contains("/Filter /DCTDecode"),
"expected the JPEG to be embedded via a DCTDecode passthrough: {text}"
);
assert!(text.contains("/DeviceRGB"), "3-component JPEG maps to DeviceRGB: {text}");
assert!(text.contains("/BitsPerComponent 8"));
assert!(
!text.contains("/FlateDecode"),
"a DCTDecode passthrough image must not ALSO be FlateDecode re-encoded: {text}"
);
let original = std::fs::read(jpeg_fixture_path()).expect("fixture must be readable");
let embedded_verbatim = bytes.windows(original.len()).any(|w| w == original.as_slice());
assert!(
embedded_verbatim,
"expected the original {}-byte JPEG embedded verbatim in the PDF, not a re-encoded copy",
original.len()
);
let decoded_rgb_len = 8 * 4 * 3;
assert_ne!(
original.len(),
decoded_rgb_len,
"sanity: fixture's JPEG size must differ from its flattened RGB8 size"
);
}