use super::*;
#[test]
fn test_compile_simple_text() {
let result = compile_to_pdf("Hello, World!", &[], None, &[], false, false).unwrap();
assert!(!result.is_empty(), "PDF bytes should not be empty");
assert!(
result.starts_with(b"%PDF"),
"PDF should start with %PDF magic bytes"
);
}
#[test]
fn test_compile_with_page_setup() {
let source = r#"#set page(width: 612pt, height: 792pt)
Hello from a US Letter page."#;
let result = compile_to_pdf(source, &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_styled_text() {
let source = r#"#text(weight: "bold", size: 16pt)[Bold Title]
#text(style: "italic")[Italic body text]
#underline[Underlined text]"#;
let result = compile_to_pdf(source, &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_colored_text() {
let source = r#"#text(fill: rgb(255, 0, 0))[Red text]
#text(fill: rgb(0, 128, 255))[Blue text]"#;
let result = compile_to_pdf(source, &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_alignment() {
let source = r#"#align(center)[Centered text]
#align(right)[Right-aligned text]"#;
let result = compile_to_pdf(source, &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_invalid_source_returns_error() {
let result = compile_to_pdf(
"#invalid-func-that-does-not-exist()",
&[],
None,
&[],
false,
false,
);
assert!(result.is_err(), "Invalid source should produce an error");
}
#[test]
fn test_compile_empty_source() {
let result = compile_to_pdf("", &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_multiple_paragraphs() {
let source = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph.";
let result = compile_to_pdf(source, &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
fn png_crc32(chunk_type: &[u8], data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in chunk_type.iter().chain(data.iter()) {
crc ^= byte as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xEDB8_8320;
} else {
crc >>= 1;
}
}
}
crc ^ 0xFFFF_FFFF
}
fn make_test_png() -> Vec<u8> {
let mut png = Vec::new();
png.extend_from_slice(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
let ihdr_data: [u8; 13] = [
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, ];
let ihdr_type = b"IHDR";
png.extend_from_slice(&(ihdr_data.len() as u32).to_be_bytes());
png.extend_from_slice(ihdr_type);
png.extend_from_slice(&ihdr_data);
png.extend_from_slice(&png_crc32(ihdr_type, &ihdr_data).to_be_bytes());
let idat_data: [u8; 15] = [
0x78, 0x01, 0x01, 0x04, 0x00, 0xFB, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, ];
let idat_type = b"IDAT";
png.extend_from_slice(&(idat_data.len() as u32).to_be_bytes());
png.extend_from_slice(idat_type);
png.extend_from_slice(&idat_data);
png.extend_from_slice(&png_crc32(idat_type, &idat_data).to_be_bytes());
let iend_type = b"IEND";
png.extend_from_slice(&0u32.to_be_bytes());
png.extend_from_slice(iend_type);
png.extend_from_slice(&png_crc32(iend_type, &[]).to_be_bytes());
png
}
fn make_test_svg() -> Vec<u8> {
br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" viewBox="0 0 1 1"><rect width="1" height="1" fill="#ff0000"/></svg>"##.to_vec()
}
#[test]
fn test_embedded_fonts_are_available() {
let world = MinimalWorld::new("", &[], &[]);
assert!(
!world.font_source.fonts().is_empty(),
"MinimalWorld should have at least the embedded fallback fonts"
);
}
#[test]
fn test_system_fonts_enabled() {
let world = MinimalWorld::new("", &[], &[]);
let embedded_only_count = {
let mut s = FontSearcher::new();
s.include_system_fonts(false);
s.search().fonts.len()
};
assert!(
world.font_source.fonts().len() >= embedded_only_count,
"System font discovery should not reduce available fonts: total {} vs embedded-only {}",
world.font_source.fonts().len(),
embedded_only_count
);
}
#[test]
fn test_compile_with_system_font_name() {
let source = r#"#set text(font: "Arial")
Hello with a system font."#;
let result = compile_to_pdf(source, &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_embedded_fonts_still_available_as_fallback() {
let source = r#"#set text(font: "Libertinus Serif")
Text in Libertinus Serif."#;
let result = compile_to_pdf(source, &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_pdfa2b_produces_valid_pdf() {
let result = compile_to_pdf(
"Hello PDF/A!",
&[],
Some(crate::config::PdfStandard::PdfA2b),
&[],
false,
false,
)
.unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_pdfa2b_contains_xmp_metadata() {
let result = compile_to_pdf(
"PDF/A metadata test",
&[],
Some(crate::config::PdfStandard::PdfA2b),
&[],
false,
false,
)
.unwrap();
let pdf_str = String::from_utf8_lossy(&result);
assert!(
pdf_str.contains("pdfaid") || pdf_str.contains("PDF/A"),
"PDF/A output should contain PDF/A identification metadata"
);
}
#[test]
fn test_compile_default_no_pdfa_metadata() {
let result = compile_to_pdf("Regular PDF", &[], None, &[], false, false).unwrap();
let pdf_str = String::from_utf8_lossy(&result);
assert!(
!pdf_str.contains("pdfaid:conformance"),
"Regular PDF should not contain PDF/A conformance metadata"
);
}
#[test]
fn test_compile_with_font_paths_empty() {
let result = compile_to_pdf("Hello!", &[], None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_with_nonexistent_font_path() {
let paths = vec![PathBuf::from("/nonexistent/font/path")];
let result = compile_to_pdf("Hello!", &[], None, &paths, false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_with_embedded_image() {
let png_data = make_test_png();
let images = vec![ImageAsset {
path: "img-0.png".to_string(),
data: png_data,
}];
let source = r#"#image("img-0.png", width: 100pt)"#;
let result = compile_to_pdf(source, &images, None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_compile_with_embedded_svg_image() {
let svg_data = make_test_svg();
let images = vec![ImageAsset {
path: "img-0.svg".to_string(),
data: svg_data,
}];
let source = r#"#image("img-0.svg", width: 100pt)"#;
let result = compile_to_pdf(source, &images, None, &[], false, false).unwrap();
assert!(!result.is_empty());
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_embedded_only_world_produces_valid_pdf() {
let world = MinimalWorld::new_embedded_only("Hello from embedded-only world!", &[]);
assert!(
!world.font_source.fonts().is_empty(),
"Embedded-only world should have fonts"
);
let warned = typst::compile::<typst::layout::PagedDocument>(&world);
let document = warned.output.expect("Compilation should succeed");
let pdf = typst_pdf::pdf(&document, &typst_pdf::PdfOptions::default())
.expect("PDF export should succeed");
assert!(pdf.starts_with(b"%PDF"));
}
#[test]
fn test_embedded_only_world_has_fonts() {
let world = MinimalWorld::new_embedded_only("", &[]);
let embedded_count = {
let mut s = FontSearcher::new();
s.include_system_fonts(false);
s.search().fonts.len()
};
assert_eq!(
world.font_source.fonts().len(),
embedded_count,
"Embedded-only world should have exactly the embedded fonts"
);
}
#[test]
fn test_pdfa_timestamp_is_not_hardcoded() {
let result = compile_to_pdf(
"Timestamp test",
&[],
Some(crate::config::PdfStandard::PdfA2b),
&[],
false,
false,
)
.unwrap();
let pdf_str = String::from_utf8_lossy(&result);
assert!(
!pdf_str.contains("2024-01-01T00:00:00"),
"PDF/A timestamp should not be the hardcoded 2024-01-01T00:00:00"
);
}
#[test]
fn test_current_utc_datetime_is_valid() {
let dt = current_utc_datetime();
let _ts = typst_pdf::Timestamp::new_utc(dt);
}
#[test]
fn test_pdfa_timestamp_has_recent_date() {
let result = compile_to_pdf(
"Year test",
&[],
Some(crate::config::PdfStandard::PdfA2b),
&[],
false,
false,
)
.unwrap();
let pdf_str = String::from_utf8_lossy(&result);
assert!(
pdf_str.contains("xmp:CreateDate") || pdf_str.contains("CreateDate"),
"PDF/A should contain creation date metadata"
);
assert!(
!pdf_str.contains("2024-01-01"),
"PDF/A timestamp should not contain hardcoded 2024-01-01"
);
}
#[test]
fn test_pdf_uses_flate_compression() {
let source = "Hello, compressed world! ".repeat(100);
let result = compile_to_pdf(&source, &[], None, &[], false, false).unwrap();
let pdf_str = String::from_utf8_lossy(&result);
assert!(
pdf_str.contains("FlateDecode"),
"PDF content streams should use FlateDecode compression"
);
}
#[test]
fn test_font_subsetting_reduces_size() {
let few_glyphs = compile_to_pdf("abcdefghij", &[], None, &[], false, false).unwrap();
let many_glyphs_source = "abcdefghijklmnopqrstuvwxyz \
ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 \
The quick brown fox jumps over the lazy dog. \
SPHINX OF BLACK QUARTZ, JUDGE MY VOW. \
Pack my box with five dozen liquor jugs. \
How vexingly quick daft zebras jump.";
let many_glyphs = compile_to_pdf(many_glyphs_source, &[], None, &[], false, false).unwrap();
assert!(
few_glyphs.len() < many_glyphs.len(),
"PDF with fewer glyphs ({} bytes) should be smaller than PDF with many glyphs ({} bytes), \
indicating font subsetting is active",
few_glyphs.len(),
many_glyphs.len()
);
}
#[test]
fn test_multipage_text_pdf_size_reasonable() {
let mut source = String::new();
for i in 1..=10 {
if i > 1 {
source.push_str("#pagebreak()\n");
}
source.push_str(&format!(
"= Page {i}\n\n\
This is page {i} of a multi-page document used to verify \
that PDF output size remains reasonable with compression \
and font subsetting enabled.\n\n\
Lorem ipsum dolor sit amet, consectetur adipiscing elit. \
Sed do eiusmod tempor incididunt ut labore et dolore magna \
aliqua. Ut enim ad minim veniam, quis nostrud exercitation \
ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\n"
));
}
let result = compile_to_pdf(&source, &[], None, &[], false, false).unwrap();
assert!(
result.len() < 512_000,
"10-page text-only PDF should be under 500KB, actual size: {} bytes ({:.1} KB)",
result.len(),
result.len() as f64 / 1024.0
);
}
#[test]
fn test_pdf_with_image_size_proportional() {
let png_data = make_test_png();
let png_size = png_data.len();
let images = vec![ImageAsset {
path: "img-0.png".to_string(),
data: png_data,
}];
let source = r#"#image("img-0.png", width: 100pt)"#;
let result = compile_to_pdf(source, &images, None, &[], false, false).unwrap();
assert!(
result.len() < 100_000,
"PDF with tiny 1x1 image should be under 100KB, actual: {} bytes ({:.1} KB). \
Image was {} bytes. Possible image re-encoding issue.",
result.len(),
result.len() as f64 / 1024.0,
png_size
);
}
#[test]
fn test_empty_page_pdf_baseline_size() {
let result = compile_to_pdf("", &[], None, &[], false, false).unwrap();
assert!(
result.len() < 100_000,
"Empty page PDF should be under 100KB (baseline), actual: {} bytes ({:.1} KB)",
result.len(),
result.len() as f64 / 1024.0
);
}
#[test]
fn test_compression_effective_for_repetitive_content() {
let short_source = "Hello world.\n\n";
let short_pdf = compile_to_pdf(short_source, &[], None, &[], false, false).unwrap();
let long_source = "Hello world.\n\n".repeat(100);
let long_pdf = compile_to_pdf(&long_source, &[], None, &[], false, false).unwrap();
let size_ratio = long_pdf.len() as f64 / short_pdf.len() as f64;
assert!(
size_ratio < 10.0,
"100x content should produce less than 10x PDF size with compression. \
Short: {} bytes, Long: {} bytes, Ratio: {:.1}x",
short_pdf.len(),
long_pdf.len(),
size_ratio
);
}
#[test]
fn test_tagged_pdf_contains_structure_tags() {
let source = "= My Heading\n\nSome paragraph text.\n\n== Sub Heading\n\nMore text.";
let result = compile_to_pdf(source, &[], None, &[], true, false).unwrap();
assert!(result.starts_with(b"%PDF"));
let pdf_str = String::from_utf8_lossy(&result);
assert!(
pdf_str.contains("StructTreeRoot") || pdf_str.contains("MarkInfo"),
"Tagged PDF should contain structure tree or mark info"
);
}
#[test]
fn test_untagged_pdf_no_structure_tree() {
let source = "= My Heading\n\nSome text.";
let result = compile_to_pdf(source, &[], None, &[], false, false).unwrap();
assert!(result.starts_with(b"%PDF"));
let pdf_str = String::from_utf8_lossy(&result);
assert!(
!pdf_str.contains("StructTreeRoot"),
"Untagged PDF should not contain StructTreeRoot"
);
}
#[test]
fn test_pdf_ua_produces_valid_pdf() {
let source = "#set document(title: \"Accessible Document\")\n= Accessible Document\n\nThis document is PDF/UA compliant.";
let result = compile_to_pdf(source, &[], None, &[], false, true).unwrap();
assert!(result.starts_with(b"%PDF"));
let pdf_str = String::from_utf8_lossy(&result);
assert!(
pdf_str.contains("pdfuaid"),
"PDF/UA output should contain pdfuaid metadata"
);
}
#[test]
fn test_pdf_ua_implies_tagged() {
let source = "#set document(title: \"Test\")\n= Heading\n\nParagraph.";
let result = compile_to_pdf(source, &[], None, &[], false, true).unwrap();
let pdf_str = String::from_utf8_lossy(&result);
assert!(
pdf_str.contains("StructTreeRoot") || pdf_str.contains("MarkInfo"),
"PDF/UA should produce tagged PDF"
);
}
#[test]
fn test_tagged_pdf_with_table() {
let source = "#table(columns: 2, [A], [B], [C], [D])";
let result = compile_to_pdf(source, &[], None, &[], true, false).unwrap();
assert!(result.starts_with(b"%PDF"));
}
#[test]
fn test_tagged_pdf_with_pdfa_combined() {
let source = "= Archival Accessible\n\nBoth standards combined.";
let result = compile_to_pdf(
source,
&[],
Some(crate::config::PdfStandard::PdfA2b),
&[],
true,
false,
)
.unwrap();
assert!(result.starts_with(b"%PDF"));
let pdf_str = String::from_utf8_lossy(&result);
assert!(pdf_str.contains("pdfaid"), "Should contain PDF/A metadata");
assert!(
pdf_str.contains("StructTreeRoot") || pdf_str.contains("MarkInfo"),
"Should contain structure tags"
);
}