mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_path_with_suffix(suffix: &str) -> String {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock moved backwards")
.as_nanos();
let mut path = std::env::temp_dir();
path.push(format!("rw_pdf_test_{}_{}", ts, suffix));
path.to_string_lossy().to_string()
}
#[test]
fn writer_embeds_font_stream_when_font_path_is_provided() {
let font_path = temp_path_with_suffix("font.ttf");
fs::write(&font_path, b"RW_TEST_FONT_BYTES").expect("write test font file");
let writer = PdfWriter::new();
let mut doc = writer
.create_document_with_font_path(
Size {
width: 595,
height: 842,
},
"Test Font",
&font_path,
)
.expect("create document with font path");
let page = doc.get_page(0).expect("page 0 must exist");
page.draw_text(
"hello",
20.0,
20.0,
12.0,
Color {
r: 0,
g: 0,
b: 0,
a: 255,
},
);
let pdf = doc.to_bytes().expect("serialize pdf");
let text = String::from_utf8_lossy(&pdf);
assert!(text.contains("/FontFile2"));
assert!(text.contains("/RWFontPath"));
assert!(text.contains("/BaseFont /Test-Font"));
assert!(text.contains("BT /F1"));
let _ = fs::remove_file(font_path);
}
#[test]
fn writer_fails_for_empty_font_file() {
let font_path = temp_path_with_suffix("empty.ttf");
fs::write(&font_path, []).expect("write empty test font file");
let writer = PdfWriter::new();
let result = writer.create_document_with_font_path(
Size {
width: 595,
height: 842,
},
"EmptyFont",
&font_path,
);
assert!(result.is_err());
let _ = fs::remove_file(font_path);
}
#[test]
fn writer_stamps_page_number_footer_when_enabled() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 595,
height: 842,
});
doc.add_page(Size {
width: 595,
height: 842,
});
doc.set_page_numbering_enabled(true);
doc.set_page_numbering_format("Page", 1);
let bytes = doc.to_bytes().expect("serialize document");
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("(Page 1/2)"));
assert!(text.contains("(Page 2/2)"));
}
#[test]
fn writer_applies_custom_page_number_layout() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 600,
height: 840,
});
doc.set_page_numbering_enabled(true);
doc.set_page_numbering_layout(100.0, 36.0, 12.0);
let bytes = doc.to_bytes().expect("serialize document");
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("BT /F1 12.00 Tf 500.00 36.00 Td (Page 1/1)"));
}
#[test]
fn reader_roundtrip_preserves_page_stream_and_media_box() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 612,
height: 792,
});
{
let page = doc.get_page(0).expect("page exists");
page.draw_text(
"hello",
20.0,
24.0,
12.0,
Color {
r: 0,
g: 0,
b: 0,
a: 255,
},
);
page.draw_line(
10.0,
10.0,
60.0,
10.0,
1.5,
Color {
r: 10,
g: 20,
b: 30,
a: 255,
},
);
page.draw_rect(
Rect {
x: 12,
y: 14,
width: 20,
height: 10,
},
1.0,
Color {
r: 40,
g: 50,
b: 60,
a: 255,
},
);
page.fill_rect(
Rect {
x: 40,
y: 20,
width: 15,
height: 8,
},
Color {
r: 70,
g: 80,
b: 90,
a: 255,
},
);
page.draw_image(
&[0xAB, 0xCD, 0xEF],
Rect {
x: 5,
y: 5,
width: 2,
height: 2,
},
);
}
let bytes = doc.to_bytes().expect("serialize");
let reader = PdfReader::new();
let mut loaded = reader.load_from_bytes(&bytes).expect("load bytes");
let page = loaded.get_page(0).expect("loaded page exists");
assert_eq!(page.size().width, 612);
assert_eq!(page.size().height, 792);
let content_bytes = page.content();
let content = String::from_utf8_lossy(&content_bytes);
assert!(content.contains("BT /F1"));
assert!(content.contains(" m "));
assert!(content.contains(" re S"));
assert!(content.contains(" re f"));
assert!(content.contains("BI"));
assert!(content.contains("EI"));
}
#[test]
fn writer_serializes_acroform_and_widget_annotations() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 595,
height: 842,
});
{
let page = doc.get_page(0).expect("page exists");
page.add_text_field(
"full_name",
Rect {
x: 40,
y: 700,
width: 200,
height: 24,
},
"Alice",
);
page.add_checkbox(
"agree",
Rect {
x: 40,
y: 660,
width: 16,
height: 16,
},
true,
);
page.add_button(
"submit",
Rect {
x: 40,
y: 620,
width: 80,
height: 24,
},
"Submit",
);
}
let bytes = doc.to_bytes().expect("serialize document");
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("/AcroForm"));
assert!(text.contains("/Annots ["));
assert!(text.contains("/Subtype /Widget"));
assert!(text.contains("/FT /Tx"));
assert!(text.contains("/FT /Btn"));
assert!(text.contains("/NeedAppearances true"));
assert!(text.contains("/T (full_name)"));
assert!(text.contains("/T (agree)"));
assert!(text.contains("/T (submit)"));
}
#[test]
fn writer_serializes_security_diagnostics_when_security_is_set() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 595,
height: 842,
});
doc.set_security(PdfSecurity {
user_password: Some("user-secret".to_string()),
owner_password: Some("owner-secret".to_string()),
print_permission: false,
edit_permission: true,
copy_permission: false,
annotation_permission: false,
});
let bytes = doc.to_bytes().expect("serialize document");
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("/RWSecurityUnsupported true"));
assert!(text.contains("/RWUserPassword (user-secret)"));
assert!(text.contains("/RWOwnerPassword (owner-secret)"));
assert!(text.contains("/RWPermPrint false"));
assert!(text.contains("/RWPermEdit true"));
assert!(text.contains("/RWPermCopy false"));
assert!(text.contains("/RWPermAnnot false"));
}
#[test]
fn reader_roundtrip_restores_security_diagnostics() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 595,
height: 842,
});
doc.set_security(PdfSecurity {
user_password: Some("u".to_string()),
owner_password: Some("o".to_string()),
print_permission: false,
edit_permission: false,
copy_permission: true,
annotation_permission: false,
});
let bytes = doc.to_bytes().expect("serialize document");
let reader = PdfReader::new();
let loaded = reader.load_from_bytes(&bytes).expect("load bytes");
let security = loaded.security();
assert_eq!(security.user_password.as_deref(), Some("u"));
assert_eq!(security.owner_password.as_deref(), Some("o"));
assert!(!security.print_permission);
assert!(!security.edit_permission);
assert!(security.copy_permission);
assert!(!security.annotation_permission);
}
#[test]
fn writer_combined_pipeline_emits_form_security_and_image_markers() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 595,
height: 842,
});
doc.set_security(PdfSecurity {
user_password: Some("combo-user".to_string()),
owner_password: Some("combo-owner".to_string()),
print_permission: false,
edit_permission: true,
copy_permission: false,
annotation_permission: true,
});
{
let page = doc.get_page(0).expect("page exists");
page.add_text_field(
"email",
Rect {
x: 32,
y: 720,
width: 240,
height: 22,
},
"alice@example.com",
);
page.add_checkbox(
"newsletter",
Rect {
x: 32,
y: 688,
width: 14,
height: 14,
},
false,
);
page.draw_image(
&[0x7F],
Rect {
x: 16,
y: 16,
width: 2,
height: 1,
},
);
}
let bytes = doc.to_bytes().expect("serialize document");
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("/AcroForm"));
assert!(text.contains("/Annots ["));
assert!(text.contains("/Subtype /Widget"));
assert!(text.contains("/T (email)"));
assert!(text.contains("/T (newsletter)"));
assert!(text.contains("/RWSecurityUnsupported true"));
assert!(text.contains("/RWUserPassword (combo-user)"));
assert!(text.contains("/RWOwnerPassword (combo-owner)"));
assert!(text.contains("% rw-image-route:raw-truncate-pad"));
}
#[test]
fn reader_roundtrip_preserves_security_and_image_route_markers() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 300,
height: 200,
});
doc.set_security(PdfSecurity {
user_password: Some("round-u".to_string()),
owner_password: Some("round-o".to_string()),
print_permission: true,
edit_permission: false,
copy_permission: false,
annotation_permission: false,
});
{
let page = doc.get_page(0).expect("page exists");
page.draw_image(
&[0x11, 0x22, 0x33],
Rect {
x: 2,
y: 2,
width: 2,
height: 2,
},
);
page.draw_text(
"ok",
10.0,
10.0,
10.0,
Color {
r: 0,
g: 0,
b: 0,
a: 255,
},
);
}
let bytes = doc.to_bytes().expect("serialize document");
let reader = PdfReader::new();
let mut loaded = reader.load_from_bytes(&bytes).expect("load bytes");
let security = loaded.security();
assert_eq!(security.user_password.as_deref(), Some("round-u"));
assert_eq!(security.owner_password.as_deref(), Some("round-o"));
assert!(security.print_permission);
assert!(!security.edit_permission);
assert!(!security.copy_permission);
assert!(!security.annotation_permission);
let page = loaded.get_page(0).expect("loaded page exists");
let content_bytes = page.content();
let content = String::from_utf8_lossy(&content_bytes);
assert!(content.contains("% rw-image-route:raw-truncate-pad"));
assert!(content.contains("% rw-image-source-len:3"));
assert!(content.contains("% rw-image-expected-rgb-len:12"));
assert!(content.contains("BT /F1"));
}
#[test]
fn writer_image_with_short_payload_uses_truncate_pad_not_tiling() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 100,
height: 100,
});
{
let page = doc.get_page(0).expect("page exists");
page.draw_image(
&[0x01, 0x02, 0x03],
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
);
}
let bytes = doc.to_bytes().expect("serialize document");
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("% rw-image-route:raw-truncate-pad"));
assert!(text.contains("% rw-image-source-len:3"));
assert!(text.contains("% rw-image-expected-rgb-len:12"));
assert!(text.contains("010203000000000000000000>"));
assert!(!text.contains("010203010203010203010203>"));
}
#[test]
fn writer_image_with_rgba_payload_drops_alpha_deterministically() {
let writer = PdfWriter::new();
let mut doc = writer.create_document(Size {
width: 100,
height: 100,
});
{
let page = doc.get_page(0).expect("page exists");
page.draw_image(
&[0x0A, 0x14, 0x1E, 0xFF],
Rect {
x: 0,
y: 0,
width: 1,
height: 1,
},
);
}
let bytes = doc.to_bytes().expect("serialize document");
let text = String::from_utf8_lossy(&bytes);
assert!(text.contains("% rw-image-route:exact-rgba-drop-alpha"));
assert!(text.contains("0A141E>"));
}
}