use super::Handle;
use crate::enhanced::bookmarks;
use crate::enhanced::page_ops;
use std::cell::RefCell;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::panic::{AssertUnwindSafe, catch_unwind};
thread_local! {
static LAST_ENHANCED_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
}
fn set_last_error(msg: String) {
LAST_ENHANCED_ERROR.with(|e| {
*e.borrow_mut() = Some(msg);
});
}
fn floor_char_boundary(s: &str, mut idx: usize) -> usize {
idx = idx.min(s.len());
while idx > 0 && !s.is_char_boundary(idx) {
idx -= 1;
}
idx
}
fn safe_slice(s: &str, start: usize, end: usize) -> &str {
let start = floor_char_boundary(s, start);
let end = floor_char_boundary(s, end);
&s[start..end]
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_get_last_enhanced_error() -> *mut c_char {
LAST_ENHANCED_ERROR.with(|e| match e.borrow_mut().take() {
Some(msg) => CString::new(msg)
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut()),
None => std::ptr::null_mut(),
})
}
pub mod document_composition;
pub mod html_to_pdf;
pub mod print_production;
pub mod security;
#[unsafe(no_mangle)]
pub extern "C" fn mp_write_pdf(_ctx: Handle, _doc: Handle, _path: *const std::ffi::c_char) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_add_blank_page(_ctx: Handle, _doc: Handle, width: f32, height: f32) -> i32 {
if width <= 0.0 || height <= 0.0 {
return -1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_merge_pdfs(
_ctx: Handle,
paths: *const *const std::ffi::c_char,
count: i32,
output_path: *const std::ffi::c_char,
) -> i32 {
match catch_unwind(AssertUnwindSafe(|| {
if paths.is_null() || output_path.is_null() || count <= 0 {
eprintln!("mp_merge_pdfs: Invalid parameters");
return -1;
}
let mut input_paths = Vec::with_capacity(count as usize);
for i in 0..count {
let path_ptr = unsafe { *paths.offset(i as isize) };
if path_ptr.is_null() {
eprintln!("mp_merge_pdfs: Null path at index {}", i);
return -1;
}
let path_str = match unsafe { CStr::from_ptr(path_ptr) }.to_str() {
Ok(s) => s.to_string(),
Err(e) => {
eprintln!("mp_merge_pdfs: Invalid UTF-8 in path {}: {}", i, e);
return -1;
}
};
input_paths.push(path_str);
}
let output_str = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(e) => {
eprintln!("mp_merge_pdfs: Invalid UTF-8 in output path: {}", e);
return -1;
}
};
match page_ops::merge_pdf(&input_paths, output_str) {
Ok(page_count) => page_count as i32,
Err(e) => {
eprintln!("mp_merge_pdfs: Merge failed: {}", e);
-1
}
}
})) {
Ok(result) => result,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_merge_pdfs: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_merge_pdfs: panic: {}", s)
} else {
"mp_merge_pdfs: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_split_pdf(
_ctx: Handle,
input_path: *const std::ffi::c_char,
output_dir: *const std::ffi::c_char,
) -> i32 {
match catch_unwind(AssertUnwindSafe(|| {
if input_path.is_null() || output_dir.is_null() {
return -1;
}
let input = match unsafe { CStr::from_ptr(input_path) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let output = match unsafe { CStr::from_ptr(output_dir) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
match page_ops::split_pdf(input, output) {
Ok(_) => 0,
Err(e) => {
eprintln!("mp_split_pdf: Split failed: {:?}", e);
-1
}
}
})) {
Ok(result) => result,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_split_pdf: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_split_pdf: panic: {}", s)
} else {
"mp_split_pdf: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_reorder_pages(
_ctx: Handle,
input_path: *const std::ffi::c_char,
output_path: *const std::ffi::c_char,
page_order: *const i32,
page_order_count: i32,
) -> i32 {
match catch_unwind(AssertUnwindSafe(|| {
if input_path.is_null()
|| output_path.is_null()
|| page_order.is_null()
|| page_order_count <= 0
{
return -1;
}
let input = match unsafe { CStr::from_ptr(input_path) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let output = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let order: Vec<usize> = (0..page_order_count as usize)
.map(|i| unsafe { *page_order.add(i) } as usize)
.collect();
match page_ops::reorder_pages(input, &order, output) {
Ok(()) => 0,
Err(e) => {
eprintln!("mp_reorder_pages: Reorder failed: {:?}", e);
-1
}
}
})) {
Ok(result) => result,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_reorder_pages: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_reorder_pages: panic: {}", s)
} else {
"mp_reorder_pages: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_add_watermark(
_ctx: Handle,
input_path: *const std::ffi::c_char,
output_path: *const std::ffi::c_char,
text: *const std::ffi::c_char,
_x: f32,
_y: f32,
font_size: f32,
opacity: f32,
) -> i32 {
if input_path.is_null() || output_path.is_null() || text.is_null() {
return -1;
}
if font_size <= 0.0 || !(0.0..=1.0).contains(&opacity) {
return -1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_overlay_pdf(
_ctx: Handle,
base_path: *const std::ffi::c_char,
output_path: *const std::ffi::c_char,
overlay_path: *const std::ffi::c_char,
_opacity: f32,
) -> i32 {
if base_path.is_null() || output_path.is_null() || overlay_path.is_null() {
return -1;
}
let base = match unsafe { CStr::from_ptr(base_path) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let output = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let overlay = match unsafe { CStr::from_ptr(overlay_path) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let base = base.to_string();
let overlay = overlay.to_string();
let output = output.to_string();
match catch_unwind(AssertUnwindSafe(|| {
crate::enhanced::overlay::merge_overlay(&base, &overlay, &output, &[])
})) {
Ok(Ok(())) => 0,
Ok(Err(e)) => {
let msg = format!("mp_overlay_pdf: Overlay merge failed: {:?}", e);
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_overlay_pdf: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_overlay_pdf: panic: {}", s)
} else {
"mp_overlay_pdf: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_optimize_pdf(_ctx: Handle, path: *const std::ffi::c_char) -> i32 {
if path.is_null() {
return -1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_linearize_pdf(
_ctx: Handle,
input_path: *const std::ffi::c_char,
output_path: *const std::ffi::c_char,
) -> i32 {
if input_path.is_null() || output_path.is_null() {
return -1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_draw_line(
_ctx: Handle,
_page: Handle,
_x0: f32,
_y0: f32,
_x1: f32,
_y1: f32,
r: f32,
g: f32,
b: f32,
alpha: f32,
line_width: f32,
) -> i32 {
if !(0.0..=1.0).contains(&r) || !(0.0..=1.0).contains(&g) || !(0.0..=1.0).contains(&b) {
return -1;
}
if !(0.0..=1.0).contains(&alpha) {
return -1;
}
if line_width <= 0.0 {
return -1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_draw_rectangle(
_ctx: Handle,
_page: Handle,
_x: f32,
_y: f32,
width: f32,
height: f32,
r: f32,
g: f32,
b: f32,
alpha: f32,
_fill: i32,
) -> i32 {
if width <= 0.0 || height <= 0.0 {
return -1;
}
if !(0.0..=1.0).contains(&r) || !(0.0..=1.0).contains(&g) || !(0.0..=1.0).contains(&b) {
return -1;
}
if !(0.0..=1.0).contains(&alpha) {
return -1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_draw_circle(
_ctx: Handle,
_page: Handle,
_x: f32,
_y: f32,
radius: f32,
r: f32,
g: f32,
b: f32,
alpha: f32,
_fill: i32,
) -> i32 {
if radius <= 0.0 {
return -1;
}
if !(0.0..=1.0).contains(&r) || !(0.0..=1.0).contains(&g) || !(0.0..=1.0).contains(&b) {
return -1;
}
if !(0.0..=1.0).contains(&alpha) {
return -1;
}
0
}
#[repr(C)]
pub struct HighlightRect {
pub page: i32, pub x: f32, pub y: f32, pub width: f32, pub height: f32, pub r: f32, pub g: f32, pub b: f32, pub alpha: f32, }
#[repr(C)]
pub struct PageDim {
pub width: f32,
pub height: f32,
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_create_highlight_overlay(
output_path: *const std::ffi::c_char,
page_dims: *const PageDim,
page_count: i32,
highlights: *const HighlightRect,
highlight_count: i32,
) -> i32 {
match catch_unwind(AssertUnwindSafe(|| {
if output_path.is_null() {
eprintln!("mp_create_highlight_overlay: Null output path");
return -1;
}
if page_count <= 0 {
eprintln!("mp_create_highlight_overlay: Invalid page count");
return -1;
}
if page_dims.is_null() {
eprintln!("mp_create_highlight_overlay: Null page dimensions");
return -1;
}
let output = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(e) => {
eprintln!("mp_create_highlight_overlay: Invalid output path: {}", e);
return -1;
}
};
let dims: Vec<(f32, f32)> = (0..page_count as usize)
.map(|i| {
let dim = unsafe { &*page_dims.add(i) };
(dim.width, dim.height)
})
.collect();
let rects: Vec<HighlightRect> = if highlights.is_null() || highlight_count <= 0 {
Vec::new()
} else {
(0..highlight_count as usize)
.map(|i| unsafe { std::ptr::read(highlights.add(i)) })
.collect()
};
let mut by_page: std::collections::HashMap<i32, Vec<&HighlightRect>> =
std::collections::HashMap::new();
for rect in &rects {
by_page.entry(rect.page).or_default().push(rect);
}
let mut writer = crate::enhanced::writer::PdfWriter::new();
for page_num in 0..page_count {
let (width, height) = dims[page_num as usize];
let page_highlights: Vec<(f32, f32, f32, f32, f32, f32, f32, f32)> =
if let Some(rects) = by_page.get(&page_num) {
rects
.iter()
.filter(|r| {
(0.0..=1.0).contains(&r.r)
&& (0.0..=1.0).contains(&r.g)
&& (0.0..=1.0).contains(&r.b)
&& (0.0..=1.0).contains(&r.alpha)
&& r.width > 0.0
&& r.height > 0.0
})
.map(|r| (r.x, r.y, r.width, r.height, r.r, r.g, r.b, r.alpha))
.collect()
} else {
Vec::new()
};
if page_highlights.is_empty() {
if let Err(e) = writer.add_blank_page(width, height) {
eprintln!(
"mp_create_highlight_overlay: Failed to add blank page {}: {:?}",
page_num, e
);
return -1;
}
} else if let Err(e) = writer.add_highlight_page(width, height, &page_highlights) {
eprintln!(
"mp_create_highlight_overlay: Failed to add highlight page {}: {:?}",
page_num, e
);
return -1;
}
}
if let Err(e) = writer.save(output) {
eprintln!("mp_create_highlight_overlay: Failed to save PDF: {:?}", e);
return -1;
}
0
})) {
Ok(result) => result,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_create_highlight_overlay: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_create_highlight_overlay: panic: {}", s)
} else {
"mp_create_highlight_overlay: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
}
}
#[repr(C)]
pub struct TextOverlayElement {
pub text: *const std::ffi::c_char,
pub x: f32,
pub y: f32,
pub height: f32,
pub font_size: f32,
pub font_name: *const std::ffi::c_char,
pub r: f32,
pub g: f32,
pub b: f32,
pub render_mode: i32,
}
#[repr(C)]
pub struct ImageOverlayElement {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub data: *const u8,
pub data_len: usize,
pub format: i32,
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_register_font(
font_name: *const std::ffi::c_char,
font_data: *const u8,
data_len: usize,
) -> u64 {
match catch_unwind(AssertUnwindSafe(|| {
use std::sync::LazyLock;
use std::sync::Mutex;
static FONT_REGISTRY: LazyLock<Mutex<std::collections::HashMap<u64, (String, Vec<u8>)>>> =
LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
static NEXT_HANDLE: LazyLock<Mutex<u64>> = LazyLock::new(|| Mutex::new(1));
if font_name.is_null() || font_data.is_null() || data_len == 0 {
eprintln!("mp_register_font: Invalid parameters");
return 0u64;
}
let name = match unsafe { CStr::from_ptr(font_name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return 0u64,
};
let data = unsafe { std::slice::from_raw_parts(font_data, data_len) }.to_vec();
if ttf_parser::Face::parse(&data, 0).is_err() {
eprintln!("mp_register_font: Invalid TTF data");
return 0u64;
}
let mut registry = FONT_REGISTRY.lock().unwrap();
let mut next = NEXT_HANDLE.lock().unwrap();
let handle = *next;
*next += 1;
registry.insert(handle, (name, data));
handle
})) {
Ok(handle) => handle,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_register_font: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_register_font: panic: {}", s)
} else {
"mp_register_font: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
0
}
}
}
fn get_registered_font(handle: u64) -> Option<(String, Vec<u8>)> {
use std::sync::LazyLock;
use std::sync::Mutex;
static FONT_REGISTRY: LazyLock<Mutex<std::collections::HashMap<u64, (String, Vec<u8>)>>> =
LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
FONT_REGISTRY.lock().ok()?.get(&handle).cloned()
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_create_text_overlay(
output_path: *const std::ffi::c_char,
width: f32,
height: f32,
font_handles: *const u64,
font_count: i32,
texts: *const TextOverlayElement,
text_count: i32,
image: *const ImageOverlayElement,
) -> i32 {
match catch_unwind(AssertUnwindSafe(|| {
use crate::enhanced::writer::{ImageElement, ImageFormat, PdfWriter, TextElement};
if output_path.is_null() {
eprintln!("mp_create_text_overlay: Null output path");
return -1;
}
if width <= 0.0 || height <= 0.0 {
eprintln!("mp_create_text_overlay: Invalid page dimensions");
return -1;
}
let output = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(e) => {
eprintln!("mp_create_text_overlay: Invalid output path: {}", e);
return -1;
}
};
let mut writer = PdfWriter::new();
if !font_handles.is_null() && font_count > 0 {
for i in 0..font_count as usize {
let handle = unsafe { *font_handles.add(i) };
if let Some((name, data)) = get_registered_font(handle) {
if let Err(e) = writer.add_ttf_font(&name, data) {
eprintln!(
"mp_create_text_overlay: Failed to add font {}: {:?}",
name, e
);
}
}
}
}
let text_elements: Vec<TextElement> = if texts.is_null() || text_count <= 0 {
Vec::new()
} else {
(0..text_count as usize)
.filter_map(|i| {
let elem = unsafe { &*texts.add(i) };
let text = if elem.text.is_null() {
return None;
} else {
match unsafe { CStr::from_ptr(elem.text) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => return None,
}
};
let font_name = if elem.font_name.is_null() {
"F1".to_string()
} else {
match unsafe { CStr::from_ptr(elem.font_name) }.to_str() {
Ok(s) => s.to_string(),
Err(_) => "F1".to_string(),
}
};
Some(TextElement {
text,
x: elem.x,
y: elem.y,
height: elem.height,
font_size: elem.font_size,
font_name,
color: (
elem.r.clamp(0.0, 1.0),
elem.g.clamp(0.0, 1.0),
elem.b.clamp(0.0, 1.0),
),
render_mode: elem.render_mode,
})
})
.collect()
};
let image_element = if image.is_null() {
None
} else {
let img = unsafe { &*image };
if img.data.is_null() || img.data_len == 0 {
None
} else {
let data = unsafe { std::slice::from_raw_parts(img.data, img.data_len) }.to_vec();
let format = if img.format == 1 {
ImageFormat::Jpeg
} else {
ImageFormat::Png
};
Some(ImageElement::new(
img.x, img.y, img.width, img.height, data, format,
))
}
};
if let Err(e) =
writer.add_text_overlay_page(width, height, &text_elements, image_element.as_ref())
{
eprintln!("mp_create_text_overlay: Failed to add page: {:?}", e);
return -1;
}
if let Err(e) = writer.save(output) {
eprintln!("mp_create_text_overlay: Failed to save PDF: {:?}", e);
return -1;
}
0
})) {
Ok(result) => result,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_create_text_overlay: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_create_text_overlay: panic: {}", s)
} else {
"mp_create_text_overlay: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_font_free(handle: u64) {
use std::sync::LazyLock;
use std::sync::Mutex;
static FONT_REGISTRY: LazyLock<Mutex<std::collections::HashMap<u64, (String, Vec<u8>)>>> =
LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
if let Ok(mut registry) = FONT_REGISTRY.lock() {
registry.remove(&handle);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_restore_bookmarks(
input_path: *const std::ffi::c_char,
output_path: *const std::ffi::c_char,
bookmarks_json: *const std::ffi::c_char,
) -> i32 {
match catch_unwind(AssertUnwindSafe(|| {
if input_path.is_null() || output_path.is_null() || bookmarks_json.is_null() {
set_last_error("mp_restore_bookmarks: Null parameter".into());
return -1;
}
let input = match unsafe { CStr::from_ptr(input_path) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("mp_restore_bookmarks: Invalid input path: {}", e));
return -1;
}
};
let output = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("mp_restore_bookmarks: Invalid output path: {}", e));
return -1;
}
};
let json_str = match unsafe { CStr::from_ptr(bookmarks_json) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("mp_restore_bookmarks: Invalid JSON: {}", e));
return -1;
}
};
let bookmarks: Vec<BookmarkJson> = match serde_json::from_str(json_str) {
Ok(b) => b,
Err(e) => {
set_last_error(format!("mp_restore_bookmarks: Failed to parse JSON: {}", e));
return -1;
}
};
let internal_bookmarks: Vec<crate::enhanced::bookmarks::Bookmark> =
bookmarks.into_iter().map(convert_bookmark_json).collect();
if internal_bookmarks.is_empty() {
if let Err(e) = std::fs::copy(input, output) {
set_last_error(format!("mp_restore_bookmarks: Failed to copy file: {}", e));
return -1;
}
return 0;
}
let raw_data = match std::fs::read(input) {
Ok(d) => d,
Err(e) => {
set_last_error(format!("mp_restore_bookmarks: Failed to read input: {}", e));
return -1;
}
};
let mut pdf_data = if crate::enhanced::page_ops::has_object_streams(&raw_data) {
match crate::enhanced::page_ops::flatten_object_streams_to_vec(&raw_data) {
Ok(flat) => flat,
Err(e) => {
set_last_error(format!(
"mp_restore_bookmarks: Failed to flatten object streams: {:?}",
e
));
return -1;
}
}
} else {
raw_data
};
let (page_objects, max_obj_num) = {
let content = String::from_utf8_lossy(&pdf_data);
let page_objects = match find_page_objects(&content, &pdf_data) {
Ok(p) => p,
Err(e) => {
set_last_error(format!(
"mp_restore_bookmarks: Failed to find page objects: {}",
e
));
return -1;
}
};
let mut max_obj_num = 0i32;
let mut search_pos = 0;
while let Some(obj_pos) = content[search_pos..].find(" 0 obj") {
let before = &content[..search_pos + obj_pos];
if let Some(last_space) =
before.rfind(|c: char| c.is_ascii_whitespace() || c == '\n')
{
if let Ok(num) = before[last_space + 1..].parse::<i32>() {
max_obj_num = max_obj_num.max(num);
}
}
search_pos = search_pos + obj_pos + 6;
}
(page_objects, max_obj_num)
};
let next_obj_num = max_obj_num + 1;
if let Err(e) = crate::enhanced::bookmark_writer::insert_bookmarks_into_pdf(
&mut pdf_data,
&internal_bookmarks,
&page_objects,
next_obj_num,
) {
set_last_error(format!(
"mp_restore_bookmarks: Failed to insert bookmarks: {:?}",
e
));
return -1;
}
if let Err(e) = std::fs::write(output, &pdf_data) {
set_last_error(format!(
"mp_restore_bookmarks: Failed to write output: {}",
e
));
return -1;
}
0
})) {
Ok(result) => result,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_restore_bookmarks: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_restore_bookmarks: panic: {}", s)
} else {
"mp_restore_bookmarks: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_add_internal_link(
pdf_path: *const std::ffi::c_char,
source_page: i32,
x0: f32,
y0: f32,
x1: f32,
y1: f32,
target_page: i32,
) -> i32 {
match catch_unwind(AssertUnwindSafe(|| {
if pdf_path.is_null() {
set_last_error("mp_add_internal_link: Null pdf_path".into());
return -1;
}
let path = match unsafe { CStr::from_ptr(pdf_path) }.to_str() {
Ok(s) => s,
Err(e) => {
set_last_error(format!("mp_add_internal_link: Invalid path: {}", e));
return -1;
}
};
if source_page < 0 || target_page < 0 {
set_last_error("mp_add_internal_link: Negative page index".into());
return -1;
}
let link = crate::micropdf::enhanced_interactive::InternalLink {
source_page: source_page as u32,
rect: (x0, y0, x1, y1),
target_page: target_page as u32,
};
if let Err(e) = crate::micropdf::enhanced_interactive::add_internal_link(path, &link) {
set_last_error(format!("mp_add_internal_link: {}", e));
return -1;
}
0
})) {
Ok(result) => result,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_add_internal_link: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_add_internal_link: panic: {}", s)
} else {
"mp_add_internal_link: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
-1
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_get_bookmarks(pdf_path: *const c_char) -> *mut c_char {
match catch_unwind(AssertUnwindSafe(|| {
if pdf_path.is_null() {
return std::ptr::null_mut();
}
let path_str = match unsafe { CStr::from_ptr(pdf_path) }.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};
let bookmarks = match bookmarks::get_bookmarks(path_str) {
Ok(b) => b,
Err(e) => {
eprintln!("mp_get_bookmarks: Failed to get bookmarks: {:?}", e);
return std::ptr::null_mut();
}
};
let json_value: serde_json::Value =
serde_json::Value::Array(bookmarks.iter().map(bookmark_to_json).collect::<Vec<_>>());
let json_str = match serde_json::to_string(&json_value) {
Ok(s) => s,
Err(e) => {
eprintln!("mp_get_bookmarks: Failed to serialize JSON: {}", e);
return std::ptr::null_mut();
}
};
match CString::new(json_str) {
Ok(c_str) => c_str.into_raw(),
Err(_) => std::ptr::null_mut(),
}
})) {
Ok(result) => result,
Err(panic_info) => {
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("mp_get_bookmarks: panic: {}", s)
} else if let Some(s) = panic_info.downcast_ref::<String>() {
format!("mp_get_bookmarks: panic: {}", s)
} else {
"mp_get_bookmarks: panic (unknown payload)".to_string()
};
eprintln!("{}", msg);
LAST_ENHANCED_ERROR.with(|cell| *cell.borrow_mut() = Some(msg));
std::ptr::null_mut()
}
}
}
fn bookmark_to_json(b: &bookmarks::Bookmark) -> serde_json::Value {
serde_json::json!({
"title": b.title,
"page": b.page,
"children": b.children.iter().map(bookmark_to_json).collect::<Vec<_>>()
})
}
#[derive(serde::Deserialize)]
struct BookmarkJson {
title: String,
page: usize,
#[serde(default)]
children: Vec<BookmarkJson>,
}
fn convert_bookmark_json(json: BookmarkJson) -> crate::enhanced::bookmarks::Bookmark {
let mut bookmark = crate::enhanced::bookmarks::Bookmark::new(json.title, json.page);
for child in json.children {
bookmark.add_child(convert_bookmark_json(child));
}
bookmark
}
fn add_bookmark_to_data(
pdf_data: &mut Vec<u8>,
bookmark: &crate::enhanced::bookmarks::Bookmark,
) -> Result<(), String> {
if crate::enhanced::page_ops::has_object_streams(pdf_data) {
let flat = crate::enhanced::page_ops::flatten_object_streams_to_vec(pdf_data)
.map_err(|e| format!("Failed to flatten object streams: {:?}", e))?;
*pdf_data = flat;
}
let (page_count, page_objects, max_obj_num) = {
let content = String::from_utf8_lossy(pdf_data);
let page_count = count_pages_in_content(&content);
let page_objects = find_page_objects(&content, pdf_data)?;
let mut max_obj_num = 0i32;
let mut search_pos = 0;
while let Some(obj_pos) = content[search_pos..].find(" 0 obj") {
let before = &content[..search_pos + obj_pos];
if let Some(last_space) = before.rfind(|c: char| c.is_ascii_whitespace() || c == '\n') {
if let Ok(num) = before[last_space + 1..].parse::<i32>() {
max_obj_num = max_obj_num.max(num);
}
}
search_pos = search_pos + obj_pos + 6;
}
(page_count, page_objects, max_obj_num)
};
if page_count == 0 {
return Err("Could not determine page count".into());
}
if bookmark.title.is_empty() {
return Err("Bookmark title cannot be empty".into());
}
if bookmark.page >= page_count {
return Err(format!(
"Page {} (0-indexed) exceeds document page count {}",
bookmark.page, page_count
));
}
let next_obj_num = max_obj_num + 1;
crate::enhanced::bookmark_writer::insert_bookmarks_into_pdf(
pdf_data,
std::slice::from_ref(bookmark),
&page_objects,
next_obj_num,
)
.map_err(|e| format!("Failed to insert bookmark: {:?}", e))?;
Ok(())
}
fn count_pages_in_content(content: &str) -> usize {
let pages_patterns = ["/Type /Pages", "/Type/Pages"];
for pattern in pages_patterns {
if let Some(pages_pos) = content.find(pattern) {
let search_region = safe_slice(content, pages_pos, pages_pos + 500);
if let Some(count_pos) = search_region.find("/Count") {
let after_count = &search_region[count_pos + 6..];
let trimmed = after_count.trim_start();
let num_end = trimmed
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(trimmed.len());
if num_end > 0 {
if let Ok(count) = trimmed[..num_end].parse::<usize>() {
if count > 0 {
return count;
}
}
}
}
}
}
let mut count = 0;
let page_patterns = ["/Type /Page", "/Type/Page"];
for pattern in page_patterns {
let pattern_len = pattern.len();
let mut pos = 0;
while let Some(found) = content[pos..].find(pattern) {
let abs_pos = pos + found;
let after = if abs_pos + pattern_len < content.len() {
&content[abs_pos + pattern_len..]
} else {
""
};
if !after.starts_with('s') && !after.starts_with('S') {
count += 1;
}
pos = abs_pos + pattern_len;
}
}
count
}
fn find_page_objects(
content: &str,
pdf_data: &[u8],
) -> Result<std::collections::HashMap<usize, i32>, String> {
let mut page_objects = std::collections::HashMap::new();
if let Some(root_pages_obj) = find_root_pages_from_catalog(content) {
if let Some(pos) = find_obj_definition(content, root_pages_obj) {
let after = safe_slice(content, pos, pos + 65536);
if let Some(kids_pos) = after.find("/Kids") {
let after_kids = &after[kids_pos + 5..];
if let Some(bracket_start) = after_kids.find('[') {
if let Some(bracket_end) = after_kids[bracket_start..].find(']') {
let kids_array =
&after_kids[bracket_start + 1..bracket_start + bracket_end];
let kid_refs = parse_obj_references(kids_array);
let mut page_idx = 1;
resolve_page_tree_kids(
&kid_refs,
content,
&mut page_objects,
&mut page_idx,
);
}
}
}
}
if !page_objects.is_empty() {
return Ok(page_objects);
}
}
let kids_patterns = ["/Type /Pages", "/Type/Pages"];
for pat in kids_patterns {
if let Some(pages_pos) = content.find(pat) {
let after_pages = safe_slice(content, pages_pos, pages_pos + 65536);
if let Some(kids_pos) = after_pages.find("/Kids") {
let after_kids = &after_pages[kids_pos + 5..];
if let Some(bracket_start) = after_kids.find('[') {
if let Some(bracket_end) = after_kids[bracket_start..].find(']') {
let kids_array =
&after_kids[bracket_start + 1..bracket_start + bracket_end];
let kid_refs = parse_obj_references(kids_array);
let mut page_idx = 1;
resolve_page_tree_kids(
&kid_refs,
content,
&mut page_objects,
&mut page_idx,
);
}
}
}
}
if !page_objects.is_empty() {
return Ok(page_objects);
}
}
let mut page_num = 1;
let patterns = ["/Type /Page", "/Type/Page"];
for pattern in patterns {
let mut search_pos = 0;
while let Some(type_pos) = content[search_pos..].find(pattern) {
let abs_type_pos = search_pos + type_pos;
let pattern_len = pattern.len();
let after_pattern = &content[abs_type_pos + pattern_len..];
if after_pattern.starts_with("s") || after_pattern.starts_with("S") {
search_pos = abs_type_pos + pattern_len;
continue;
}
let before = &content[..abs_type_pos];
if let Some(obj_pos) = before.rfind(" 0 obj") {
let before_obj = &before[..obj_pos];
let obj_num: i32 = before_obj
.chars()
.rev()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.chars()
.rev()
.collect::<String>()
.parse()
.unwrap_or(0);
if obj_num > 0 && !page_objects.values().any(|&v| v == obj_num) {
page_objects.insert(page_num, obj_num);
page_num += 1;
}
}
search_pos = abs_type_pos + pattern_len;
}
}
if !page_objects.is_empty() {
return Ok(page_objects);
}
let has_objstm = {
fn has_pattern(data: &[u8], pat: &[u8]) -> bool {
data.windows(pat.len()).any(|w| w == pat)
}
has_pattern(pdf_data, b"/Type /ObjStm") || has_pattern(pdf_data, b"/Type/ObjStm")
};
if has_objstm {
match crate::enhanced::object_stream::parse_all_object_streams(pdf_data) {
Ok(all_objects) => {
let mut obj_lookup: std::collections::HashMap<i32, &str> =
std::collections::HashMap::new();
for (num, data) in &all_objects {
obj_lookup.insert(*num, data.as_str());
}
let mut root_pages_obj: Option<(i32, i32)> = None;
for (&obj_num, data) in &obj_lookup {
if (data.contains("/Type /Pages") || data.contains("/Type/Pages"))
&& data.contains("/Count")
{
if let Some(count) = extract_int_from_dict(data, "/Count") {
if root_pages_obj.map_or(true, |(_, c)| count > c) {
root_pages_obj = Some((obj_num, count));
}
}
}
}
if let Some((root_obj, _)) = root_pages_obj {
if let Some(root_data) = obj_lookup.get(&root_obj) {
let kid_refs = parse_obj_references_from_kids(root_data);
let mut page_idx = 1;
resolve_page_tree_kids_from_objects(
&kid_refs,
&obj_lookup,
content,
&mut page_objects,
&mut page_idx,
);
}
}
if page_objects.is_empty() {
let mut page_idx = 1;
let mut page_entries: Vec<i32> = Vec::new();
for (&obj_num, data) in &obj_lookup {
if (data.contains("/Type /Page") || data.contains("/Type/Page"))
&& !data.contains("/Type /Pages")
&& !data.contains("/Type/Pages")
{
page_entries.push(obj_num);
}
}
page_entries.sort();
for obj_num in page_entries {
page_objects.insert(page_idx, obj_num);
page_idx += 1;
}
}
}
Err(e) => {
eprintln!("find_page_objects: object stream parsing failed: {:?}", e);
}
}
}
if page_objects.is_empty() {
if let Ok(all_objects) = crate::enhanced::object_stream::parse_all_object_streams(pdf_data)
{
if !all_objects.is_empty() {
let mut page_idx = 1;
let mut page_entries: Vec<i32> = Vec::new();
for (&obj_num, data) in &all_objects {
let is_page = (data.contains("/Type /Page") || data.contains("/Type/Page"))
&& !data.contains("/Type /Pages")
&& !data.contains("/Type/Pages");
if is_page {
page_entries.push(obj_num);
}
}
page_entries.sort();
for obj_num in page_entries {
page_objects.insert(page_idx, obj_num);
page_idx += 1;
}
}
}
}
if page_objects.is_empty() {
return Err("No page objects found".into());
}
Ok(page_objects)
}
fn find_obj_definition(content: &str, obj_num: i32) -> Option<usize> {
let pattern = format!("{} 0 obj", obj_num);
let mut search_pos = 0;
while let Some(rel) = content[search_pos..].find(&pattern) {
let abs = search_pos + rel;
let preceded_ok = abs == 0
|| content
.as_bytes()
.get(abs - 1)
.map_or(false, |&b| b == b'\n' || b == b'\r' || b == b' ');
let after_pos = abs + pattern.len();
let followed_ok = content.as_bytes().get(after_pos).map_or(true, |&b| {
b == b'\n' || b == b'\r' || b == b' ' || b == b'<'
});
if preceded_ok && followed_ok {
return Some(abs);
}
search_pos = abs + pattern.len();
}
None
}
fn find_root_pages_from_catalog(content: &str) -> Option<i32> {
for cat_pat in ["/Type /Catalog", "/Type/Catalog"] {
if let Some(cat_pos) = content.find(cat_pat) {
let region = safe_slice(content, cat_pos, cat_pos + 2048);
if let Some(pages_pos) = region.find("/Pages") {
let after_pages = ®ion[pages_pos + 6..];
let trimmed = after_pages.trim_start();
let num: String = trimmed.chars().take_while(|c| c.is_ascii_digit()).collect();
if let Ok(n) = num.parse::<i32>() {
return Some(n);
}
}
}
}
let mut best_obj: Option<i32> = None;
let mut best_count: i32 = 0;
for pages_pat in ["/Type/Pages", "/Type /Pages"] {
let mut search_pos = 0;
while let Some(rel_pos) = content[search_pos..].find(pages_pat) {
let abs_pos = search_pos + rel_pos;
let scan_start = abs_pos.saturating_sub(8192);
let before = safe_slice(content, scan_start, abs_pos);
if let Some(obj_keyword) = before.rfind(" 0 obj") {
let num_region = &before[..obj_keyword];
let obj_num: i32 = num_region
.chars()
.rev()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.chars()
.rev()
.collect::<String>()
.parse()
.unwrap_or(0);
if obj_num > 0 {
let obj_start = scan_start
+ (num_region.len()
- num_region
.chars()
.rev()
.take_while(|c| c.is_ascii_digit())
.count());
let obj_region = safe_slice(content, obj_start, abs_pos + 256);
if let Some(count_pos) = obj_region.find("/Count") {
let after_count = &obj_region[count_pos + 6..];
let count_str: String = after_count
.trim_start()
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if let Ok(count) = count_str.parse::<i32>() {
if count > best_count {
best_count = count;
best_obj = Some(obj_num);
}
}
}
}
}
search_pos = abs_pos + pages_pat.len();
}
}
best_obj
}
fn parse_obj_references(s: &str) -> Vec<i32> {
let parts: Vec<&str> = s.split_whitespace().collect();
let mut refs = Vec::new();
let mut i = 0;
while i + 2 < parts.len() {
if parts[i + 2] == "R" {
if let Ok(obj_num) = parts[i].parse::<i32>() {
refs.push(obj_num);
}
}
i += 3;
}
refs
}
fn parse_obj_references_from_kids(data: &str) -> Vec<i32> {
if let Some(kids_pos) = data.find("/Kids") {
let after = &data[kids_pos + 5..];
if let Some(start) = after.find('[') {
if let Some(end) = after[start..].find(']') {
return parse_obj_references(&after[start + 1..start + end]);
}
}
}
Vec::new()
}
fn resolve_page_tree_kids(
kid_refs: &[i32],
content: &str,
page_objects: &mut std::collections::HashMap<usize, i32>,
page_idx: &mut usize,
) {
for &obj_num in kid_refs {
if let Some(pos) = find_obj_definition(content, obj_num) {
let window = safe_slice(content, pos, pos + 65536);
let obj_body = if let Some(end) = window.find("endobj") {
&window[..end]
} else {
window
};
let is_pages = obj_body.contains("/Type /Pages") || obj_body.contains("/Type/Pages");
if is_pages {
if let Some(kids_pos) = obj_body.find("/Kids") {
let after_kids = &obj_body[kids_pos + 5..];
if let Some(bs) = after_kids.find('[') {
if let Some(be) = after_kids[bs..].find(']') {
let child_refs = parse_obj_references(&after_kids[bs + 1..bs + be]);
resolve_page_tree_kids(&child_refs, content, page_objects, page_idx);
}
}
}
} else {
page_objects.insert(*page_idx, obj_num);
*page_idx += 1;
}
} else {
page_objects.insert(*page_idx, obj_num);
*page_idx += 1;
}
}
}
fn resolve_page_tree_kids_from_objects(
kid_refs: &[i32],
obj_lookup: &std::collections::HashMap<i32, &str>,
content: &str,
page_objects: &mut std::collections::HashMap<usize, i32>,
page_idx: &mut usize,
) {
for &obj_num in kid_refs {
let obj_data: Option<String> =
obj_lookup.get(&obj_num).map(|s| s.to_string()).or_else(|| {
let header = format!("{} 0 obj", obj_num);
content
.find(&header)
.map(|pos| safe_slice(content, pos, pos + 4096).to_string())
});
if let Some(data) = obj_data {
let is_pages = data.contains("/Type /Pages") || data.contains("/Type/Pages");
if is_pages {
let child_refs = parse_obj_references_from_kids(&data);
resolve_page_tree_kids_from_objects(
&child_refs,
obj_lookup,
content,
page_objects,
page_idx,
);
} else {
page_objects.insert(*page_idx, obj_num);
*page_idx += 1;
}
} else {
page_objects.insert(*page_idx, obj_num);
*page_idx += 1;
}
}
}
fn extract_int_from_dict(dict: &str, key: &str) -> Option<i32> {
dict.find(key).and_then(|pos| {
let after = &dict[pos + key.len()..];
let num_str: String = after
.chars()
.skip_while(|c| c.is_ascii_whitespace())
.take_while(|c| c.is_ascii_digit())
.collect();
num_str.parse().ok()
})
}
fn find_or_create_outlines(pdf_data: &mut Vec<u8>) -> Result<i32, String> {
let content = String::from_utf8_lossy(pdf_data);
if let Some(outlines_pos) = content.find("/Outlines") {
let after_outlines = &content[outlines_pos + 9..];
let parts: Vec<&str> = after_outlines.split_whitespace().take(2).collect();
if parts.len() >= 2 && parts[1] == "0" {
if let Ok(outline_obj_num) = parts[0].parse::<i32>() {
return Ok(outline_obj_num);
}
}
}
let mut max_obj_num = 0;
let mut catalog_obj_num = 0;
let mut search_pos = 0;
while let Some(obj_pos) = content[search_pos..].find(" 0 obj") {
let abs_pos = search_pos + obj_pos;
let before = &content[..abs_pos];
if let Some(last_space) = before.rfind(|c: char| c.is_ascii_whitespace() || c == '\n') {
if let Ok(num) = before[last_space + 1..].parse::<i32>() {
max_obj_num = max_obj_num.max(num);
let after = &content[abs_pos..];
if after.contains("/Type /Catalog") || after.contains("/Type/Catalog") {
catalog_obj_num = num;
}
}
}
search_pos = abs_pos + 6;
}
if catalog_obj_num == 0 {
return Err("Catalog object not found".into());
}
let outline_obj_num = max_obj_num + 1;
let outlines_obj = format!(
"{} 0 obj\n<<\n/Type /Outlines\n/Count 0\n>>\nendobj\n",
outline_obj_num
);
let insert_pos = {
let xref_patterns: &[&[u8]] = &[b"\nxref\n", b"\nxref ", b"xref\n"];
let mut found_pos = None;
for pattern in xref_patterns {
if let Some(pos) = pdf_data.windows(pattern.len()).position(|w| w == *pattern) {
found_pos = Some(pos);
break;
}
}
if found_pos.is_none() {
if let Some(pos) = pdf_data.windows(9).position(|w| w == b"startxref") {
let mut start = pos;
while start > 0 && pdf_data[start - 1] != b'\n' {
start -= 1;
}
found_pos = Some(start);
}
}
if found_pos.is_none() {
if let Some(pos) = pdf_data.windows(5).position(|w| w == b"%%EOF") {
let mut start = pos;
while start > 0 && pdf_data[start - 1] != b'\n' {
start -= 1;
}
found_pos = Some(start);
}
}
found_pos.ok_or("Could not find insertion point (no xref, startxref, or %%EOF)")?
};
for (i, byte) in outlines_obj.as_bytes().iter().enumerate() {
pdf_data.insert(insert_pos + i, *byte);
}
let catalog_pattern = format!("{} 0 obj", catalog_obj_num);
if let Some(catalog_pos) = pdf_data
.windows(catalog_pattern.len())
.position(|w| w == catalog_pattern.as_bytes())
{
let catalog_section = &pdf_data[catalog_pos..];
if let Some(end_pos) = catalog_section.windows(2).position(|w| w == b">>") {
let insert_pos = catalog_pos + end_pos;
let outlines_ref = format!("/Outlines {} 0 R\n", outline_obj_num);
for (i, byte) in outlines_ref.as_bytes().iter().enumerate() {
pdf_data.insert(insert_pos + i, *byte);
}
}
}
Ok(outline_obj_num)
}
use serde;
use serde_json;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_blank_page_invalid_dimensions() {
assert_eq!(mp_add_blank_page(0, 0, -10.0, 100.0), -1);
assert_eq!(mp_add_blank_page(0, 0, 100.0, 0.0), -1);
}
#[test]
fn test_merge_pdfs_null_paths() {
assert_eq!(
mp_merge_pdfs(0, std::ptr::null(), 0, c"out.pdf".as_ptr()),
-1
);
}
#[test]
fn test_split_pdf_null_path() {
assert_eq!(mp_split_pdf(0, std::ptr::null(), c"/tmp".as_ptr()), -1);
}
#[test]
fn test_add_watermark_null_text() {
assert_eq!(
mp_add_watermark(
0,
c"in.pdf".as_ptr(),
c"out.pdf".as_ptr(),
std::ptr::null(),
0.0,
0.0,
12.0,
0.5
),
-1
);
}
#[test]
fn test_add_watermark_invalid_opacity() {
assert_eq!(
mp_add_watermark(
0,
c"in.pdf".as_ptr(),
c"out.pdf".as_ptr(),
c"TEST".as_ptr(),
0.0,
0.0,
12.0,
1.5
),
-1
);
}
#[test]
fn test_draw_line_invalid_color() {
assert_eq!(
mp_draw_line(0, 0, 0.0, 0.0, 100.0, 100.0, 1.5, 0.5, 0.5, 1.0, 1.0),
-1
);
}
#[test]
fn test_draw_rectangle_invalid_dimensions() {
assert_eq!(
mp_draw_rectangle(0, 0, 0.0, 0.0, -10.0, 100.0, 0.5, 0.5, 0.5, 1.0, 1),
-1
);
}
#[test]
fn test_draw_circle_invalid_radius() {
assert_eq!(
mp_draw_circle(0, 0, 50.0, 50.0, -10.0, 0.5, 0.5, 0.5, 1.0, 1),
-1
);
}
#[test]
fn test_insert_bookmarks_removes_old_and_creates_catalog() {
let tmp = tempfile::TempDir::new().unwrap();
let input_path = tmp.path().join("multipage.pdf");
let output1 = tmp.path().join("out1.pdf");
let output2 = tmp.path().join("out2.pdf");
let doc = crate::cpdf::document::CpdfDocument::blank(612.0, 792.0, 5).unwrap();
std::fs::write(&input_path, doc.to_bytes()).unwrap();
let input_c = std::ffi::CString::new(input_path.to_str().unwrap()).unwrap();
let out1_c = std::ffi::CString::new(output1.to_str().unwrap()).unwrap();
let out2_c = std::ffi::CString::new(output2.to_str().unwrap()).unwrap();
let json = r#"[
{"title":"Root","page":0,"children":[
{"title":"Page 2","page":1,"children":[]}
]}
]"#;
let json_c = std::ffi::CString::new(json).unwrap();
let ret = mp_restore_bookmarks(input_c.as_ptr(), out1_c.as_ptr(), json_c.as_ptr());
assert_eq!(ret, 0, "First bookmark insertion should succeed");
let pdf_data = std::fs::read(&output1).unwrap();
let content = String::from_utf8_lossy(&pdf_data);
let first_dests = content.matches("/Dest [").count();
assert_eq!(
first_dests, 2,
"Should have 2 bookmarks after first insertion"
);
let json2 = r#"[{"title":"Only","page":0,"children":[]}]"#;
let json2_c = std::ffi::CString::new(json2).unwrap();
let ret = mp_restore_bookmarks(out1_c.as_ptr(), out2_c.as_ptr(), json2_c.as_ptr());
assert_eq!(ret, 0, "Second bookmark insertion should succeed");
let pdf_data2 = std::fs::read(&output2).unwrap();
let content2 = String::from_utf8_lossy(&pdf_data2);
let second_dests = content2.matches("/Dest [").count();
assert_eq!(
second_dests, 1,
"Old bookmarks should be removed; only 1 new bookmark expected, found {}",
second_dests
);
assert!(
content2.contains("/Type /Outlines"),
"Output should have Outlines"
);
}
#[test]
fn test_merge_pdfs_null_output_path() {
let path = std::ffi::CString::new("in.pdf").unwrap();
let paths = [path.as_ptr()];
assert_eq!(mp_merge_pdfs(0, paths.as_ptr(), 1, std::ptr::null()), -1);
}
#[test]
fn test_merge_pdfs_invalid_count() {
let path = std::ffi::CString::new("out.pdf").unwrap();
assert_eq!(mp_merge_pdfs(0, std::ptr::null(), -1, path.as_ptr()), -1);
}
#[test]
fn test_split_pdf_null_output_dir() {
let path = std::ffi::CString::new("/tmp/in.pdf").unwrap();
assert_eq!(mp_split_pdf(0, path.as_ptr(), std::ptr::null()), -1);
}
#[test]
fn test_reorder_pages_null_input() {
let out = std::ffi::CString::new("/tmp/out.pdf").unwrap();
let order = [0i32, 1i32];
assert_eq!(
mp_reorder_pages(0, std::ptr::null(), out.as_ptr(), order.as_ptr(), 2),
-1
);
}
#[test]
fn test_reorder_pages_null_order() {
let inp = std::ffi::CString::new("/tmp/in.pdf").unwrap();
let out = std::ffi::CString::new("/tmp/out.pdf").unwrap();
assert_eq!(
mp_reorder_pages(0, inp.as_ptr(), out.as_ptr(), std::ptr::null(), 2),
-1
);
}
#[test]
fn test_add_watermark_null_paths() {
assert_eq!(
mp_add_watermark(
0,
std::ptr::null(),
c"out.pdf".as_ptr(),
c"T".as_ptr(),
0.0,
0.0,
12.0,
0.5
),
-1
);
assert_eq!(
mp_add_watermark(
0,
c"in.pdf".as_ptr(),
std::ptr::null(),
c"T".as_ptr(),
0.0,
0.0,
12.0,
0.5
),
-1
);
}
#[test]
fn test_add_watermark_invalid_font_size() {
assert_eq!(
mp_add_watermark(
0,
c"in.pdf".as_ptr(),
c"out.pdf".as_ptr(),
c"T".as_ptr(),
0.0,
0.0,
0.0,
0.5
),
-1
);
}
#[test]
fn test_overlay_pdf_null_paths() {
assert_eq!(
mp_overlay_pdf(
0,
std::ptr::null(),
c"out.pdf".as_ptr(),
c"over.pdf".as_ptr(),
1.0
),
-1
);
assert_eq!(
mp_overlay_pdf(
0,
c"base.pdf".as_ptr(),
std::ptr::null(),
c"over.pdf".as_ptr(),
1.0
),
-1
);
assert_eq!(
mp_overlay_pdf(
0,
c"base.pdf".as_ptr(),
c"out.pdf".as_ptr(),
std::ptr::null(),
1.0
),
-1
);
}
#[test]
fn test_optimize_pdf_null_path() {
assert_eq!(mp_optimize_pdf(0, std::ptr::null()), -1);
}
#[test]
fn test_linearize_pdf_null_paths() {
assert_eq!(
mp_linearize_pdf(0, std::ptr::null(), c"out.pdf".as_ptr()),
-1
);
assert_eq!(
mp_linearize_pdf(0, c"in.pdf".as_ptr(), std::ptr::null()),
-1
);
}
#[test]
fn test_draw_line_invalid_alpha() {
assert_eq!(
mp_draw_line(0, 0, 0.0, 0.0, 100.0, 100.0, 0.5, 0.5, 0.5, 1.5, 1.0),
-1
);
assert_eq!(
mp_draw_line(0, 0, 0.0, 0.0, 100.0, 100.0, 0.5, 0.5, 0.5, -0.1, 1.0),
-1
);
}
#[test]
fn test_draw_line_invalid_line_width() {
assert_eq!(
mp_draw_line(0, 0, 0.0, 0.0, 100.0, 100.0, 0.5, 0.5, 0.5, 1.0, 0.0),
-1
);
}
#[test]
fn test_draw_rectangle_invalid_alpha() {
assert_eq!(
mp_draw_rectangle(0, 0, 0.0, 0.0, 100.0, 100.0, 0.5, 0.5, 0.5, 1.5, 1),
-1
);
}
#[test]
fn test_draw_circle_invalid_alpha() {
assert_eq!(
mp_draw_circle(0, 0, 50.0, 50.0, 10.0, 0.5, 0.5, 0.5, 1.5, 1),
-1
);
}
#[test]
fn test_get_last_enhanced_error() {
let err = mp_get_last_enhanced_error();
assert!(err.is_null() || unsafe { std::ffi::CStr::from_ptr(err).to_str().is_ok() });
}
#[test]
fn test_mp_get_bookmarks_null_path() {
assert!(mp_get_bookmarks(std::ptr::null()).is_null());
}
#[test]
fn test_mp_add_blank_page_valid() {
assert_eq!(mp_add_blank_page(0, 0, 100.0, 100.0), 0);
}
#[test]
fn test_mp_merge_pdfs_success() {
let tmp = tempfile::TempDir::new().unwrap();
let input1 = tmp.path().join("a.pdf");
let input2 = tmp.path().join("b.pdf");
let output = tmp.path().join("merged.pdf");
let doc = crate::cpdf::document::CpdfDocument::blank(612.0, 792.0, 2).unwrap();
std::fs::write(&input1, doc.to_bytes()).unwrap();
std::fs::write(&input2, doc.to_bytes()).unwrap();
let p1 = std::ffi::CString::new(input1.to_str().unwrap()).unwrap();
let p2 = std::ffi::CString::new(input2.to_str().unwrap()).unwrap();
let paths = [p1.as_ptr(), p2.as_ptr()];
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
let ret = mp_merge_pdfs(0, paths.as_ptr(), 2, out_c.as_ptr());
assert!(ret > 0);
}
#[test]
fn test_mp_split_pdf_success() {
let tmp = tempfile::TempDir::new().unwrap();
let input = tmp.path().join("in.pdf");
let output_dir = tmp.path().join("pages");
std::fs::create_dir_all(&output_dir).unwrap();
let doc = crate::cpdf::document::CpdfDocument::blank(612.0, 792.0, 3).unwrap();
std::fs::write(&input, doc.to_bytes()).unwrap();
let in_c = std::ffi::CString::new(input.to_str().unwrap()).unwrap();
let out_c = std::ffi::CString::new(output_dir.to_str().unwrap()).unwrap();
assert_eq!(mp_split_pdf(0, in_c.as_ptr(), out_c.as_ptr()), 0);
}
#[test]
fn test_mp_reorder_pages_success() {
let tmp = tempfile::TempDir::new().unwrap();
let input = tmp.path().join("in.pdf");
let output = tmp.path().join("out.pdf");
let doc = crate::cpdf::document::CpdfDocument::blank(612.0, 792.0, 3).unwrap();
std::fs::write(&input, doc.to_bytes()).unwrap();
let in_c = std::ffi::CString::new(input.to_str().unwrap()).unwrap();
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
let order = [2i32, 0i32, 1i32];
assert_eq!(
mp_reorder_pages(0, in_c.as_ptr(), out_c.as_ptr(), order.as_ptr(), 3),
0
);
}
#[test]
fn test_mp_add_watermark_valid() {
let in_c = std::ffi::CString::new("/tmp/in.pdf").unwrap();
let out_c = std::ffi::CString::new("/tmp/out.pdf").unwrap();
let text_c = std::ffi::CString::new("WATERMARK").unwrap();
assert_eq!(
mp_add_watermark(
0,
in_c.as_ptr(),
out_c.as_ptr(),
text_c.as_ptr(),
0.0,
0.0,
12.0,
0.5
),
0
);
}
#[test]
fn test_mp_optimize_pdf_valid() {
let path = std::ffi::CString::new("/tmp/test.pdf").unwrap();
assert_eq!(mp_optimize_pdf(0, path.as_ptr()), 0);
}
#[test]
fn test_mp_linearize_pdf_valid() {
let in_c = std::ffi::CString::new("/tmp/in.pdf").unwrap();
let out_c = std::ffi::CString::new("/tmp/out.pdf").unwrap();
assert_eq!(mp_linearize_pdf(0, in_c.as_ptr(), out_c.as_ptr()), 0);
}
#[test]
fn test_mp_draw_line_valid() {
assert_eq!(
mp_draw_line(0, 0, 0.0, 0.0, 100.0, 100.0, 0.5, 0.5, 0.5, 1.0, 1.0),
0
);
}
#[test]
fn test_mp_draw_rectangle_valid() {
assert_eq!(
mp_draw_rectangle(0, 0, 0.0, 0.0, 100.0, 100.0, 0.5, 0.5, 0.5, 1.0, 1),
0
);
}
#[test]
fn test_mp_draw_circle_valid() {
assert_eq!(
mp_draw_circle(0, 0, 50.0, 50.0, 10.0, 0.5, 0.5, 0.5, 1.0, 1),
0
);
}
#[test]
fn test_mp_get_last_enhanced_error_after_error() {
let _ = mp_merge_pdfs(0, std::ptr::null(), 0, c"out.pdf".as_ptr());
let err = mp_get_last_enhanced_error();
if !err.is_null() {
let _ = unsafe { std::ffi::CString::from_raw(err) };
}
}
#[test]
fn test_mp_add_watermark_valid_draw_rectangle() {
let in_c = std::ffi::CString::new("/tmp/in.pdf").unwrap();
let out_c = std::ffi::CString::new("/tmp/out.pdf").unwrap();
let text_c = std::ffi::CString::new("X").unwrap();
assert_eq!(
mp_add_watermark(
0,
in_c.as_ptr(),
out_c.as_ptr(),
text_c.as_ptr(),
10.0,
10.0,
24.0,
1.0
),
0
);
}
#[test]
fn test_mp_overlay_pdf_success() {
let tmp = tempfile::TempDir::new().unwrap();
let base = tmp.path().join("base.pdf");
let overlay = tmp.path().join("overlay.pdf");
let output = tmp.path().join("out.pdf");
let doc = crate::cpdf::document::CpdfDocument::blank(612.0, 792.0, 1).unwrap();
std::fs::write(&base, doc.to_bytes()).unwrap();
std::fs::write(&overlay, doc.to_bytes()).unwrap();
let base_c = std::ffi::CString::new(base.to_str().unwrap()).unwrap();
let overlay_c = std::ffi::CString::new(overlay.to_str().unwrap()).unwrap();
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
assert_eq!(
mp_overlay_pdf(0, base_c.as_ptr(), out_c.as_ptr(), overlay_c.as_ptr(), 1.0),
0
);
}
#[test]
fn test_mp_create_highlight_overlay() {
let tmp = tempfile::TempDir::new().unwrap();
let output = tmp.path().join("overlay.pdf");
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
let dims = [super::PageDim {
width: 612.0,
height: 792.0,
}];
let rects = [super::HighlightRect {
page: 0,
x: 100.0,
y: 100.0,
width: 200.0,
height: 50.0,
r: 1.0,
g: 0.0,
b: 0.0,
alpha: 0.5,
}];
assert_eq!(
mp_create_highlight_overlay(out_c.as_ptr(), dims.as_ptr(), 1, rects.as_ptr(), 1),
0
);
}
#[test]
fn test_mp_create_highlight_overlay_blank_pages() {
let tmp = tempfile::TempDir::new().unwrap();
let output = tmp.path().join("overlay.pdf");
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
let dims = [
super::PageDim {
width: 612.0,
height: 792.0,
},
super::PageDim {
width: 595.0,
height: 842.0,
},
];
assert_eq!(
mp_create_highlight_overlay(out_c.as_ptr(), dims.as_ptr(), 2, std::ptr::null(), 0),
0
);
}
#[test]
fn test_mp_get_bookmarks_success() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("doc.pdf");
let doc = crate::cpdf::document::CpdfDocument::blank(612.0, 792.0, 2).unwrap();
std::fs::write(&path, doc.to_bytes()).unwrap();
let path_c = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let result = mp_get_bookmarks(path_c.as_ptr());
assert!(!result.is_null());
}
#[test]
fn test_mp_create_highlight_overlay_null_output() {
let dims = [super::PageDim {
width: 612.0,
height: 792.0,
}];
assert_eq!(
mp_create_highlight_overlay(std::ptr::null(), dims.as_ptr(), 1, std::ptr::null(), 0),
-1
);
}
#[test]
fn test_mp_create_highlight_overlay_invalid_page_count() {
let tmp = tempfile::TempDir::new().unwrap();
let output = tmp.path().join("out.pdf");
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
let dims = [super::PageDim {
width: 612.0,
height: 792.0,
}];
assert_eq!(
mp_create_highlight_overlay(out_c.as_ptr(), dims.as_ptr(), 0, std::ptr::null(), 0),
-1
);
}
#[test]
fn test_mp_create_highlight_overlay_null_page_dims() {
let tmp = tempfile::TempDir::new().unwrap();
let output = tmp.path().join("out.pdf");
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
assert_eq!(
mp_create_highlight_overlay(out_c.as_ptr(), std::ptr::null(), 1, std::ptr::null(), 0),
-1
);
}
#[test]
fn test_mp_create_text_overlay_null_output() {
assert_eq!(
mp_create_text_overlay(
std::ptr::null(),
612.0,
792.0,
std::ptr::null(),
0,
std::ptr::null(),
0,
std::ptr::null(),
),
-1
);
}
#[test]
fn test_mp_create_text_overlay_invalid_dimensions() {
let tmp = tempfile::TempDir::new().unwrap();
let output = tmp.path().join("out.pdf");
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
assert_eq!(
mp_create_text_overlay(
out_c.as_ptr(),
-1.0,
792.0,
std::ptr::null(),
0,
std::ptr::null(),
0,
std::ptr::null(),
),
-1
);
assert_eq!(
mp_create_text_overlay(
out_c.as_ptr(),
612.0,
0.0,
std::ptr::null(),
0,
std::ptr::null(),
0,
std::ptr::null(),
),
-1
);
}
#[test]
fn test_mp_register_font_null_name() {
let data = b"dummy";
assert_eq!(
mp_register_font(std::ptr::null(), data.as_ptr(), data.len()),
0
);
}
#[test]
fn test_mp_register_font_null_data() {
let name = std::ffi::CString::new("F1").unwrap();
assert_eq!(mp_register_font(name.as_ptr(), std::ptr::null(), 100), 0);
}
#[test]
fn test_mp_register_font_zero_len() {
let name = std::ffi::CString::new("F1").unwrap();
let data = [0u8; 10];
assert_eq!(mp_register_font(name.as_ptr(), data.as_ptr(), 0), 0);
}
#[test]
fn test_mp_register_font_invalid_ttf() {
let name = std::ffi::CString::new("F1").unwrap();
let data = b"not a valid TTF";
assert_eq!(
mp_register_font(name.as_ptr(), data.as_ptr(), data.len()),
0
);
}
#[test]
fn test_mp_font_free_noop() {
mp_font_free(0);
}
#[test]
fn test_mp_create_text_overlay_empty_success() {
let tmp = tempfile::TempDir::new().unwrap();
let output = tmp.path().join("text_overlay.pdf");
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
assert_eq!(
mp_create_text_overlay(
out_c.as_ptr(),
612.0,
792.0,
std::ptr::null(),
0,
std::ptr::null(),
0,
std::ptr::null(),
),
0
);
assert!(output.exists());
}
#[test]
fn test_mp_create_highlight_overlay_filter_invalid_highlights() {
let tmp = tempfile::TempDir::new().unwrap();
let output = tmp.path().join("overlay.pdf");
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
let dims = [super::PageDim {
width: 612.0,
height: 792.0,
}];
let rects = [
super::HighlightRect {
page: 0,
x: 100.0,
y: 100.0,
width: -1.0,
height: 50.0,
r: 0.5,
g: 0.5,
b: 0.5,
alpha: 0.5,
},
super::HighlightRect {
page: 0,
x: 100.0,
y: 100.0,
width: 200.0,
height: 50.0,
r: 1.0,
g: 0.0,
b: 0.0,
alpha: 0.5,
},
];
assert_eq!(
mp_create_highlight_overlay(out_c.as_ptr(), dims.as_ptr(), 1, rects.as_ptr(), 2),
0
);
}
#[test]
fn test_mp_merge_pdfs_null_path_in_array() {
let out_c = std::ffi::CString::new("/tmp/out.pdf").unwrap();
let a_c = std::ffi::CString::new("/tmp/a.pdf").unwrap();
let paths: [*const std::ffi::c_char; 2] = [a_c.as_ptr(), std::ptr::null()];
assert_eq!(mp_merge_pdfs(0, paths.as_ptr(), 2, out_c.as_ptr()), -1);
}
#[test]
fn test_mp_write_pdf() {
assert_eq!(mp_write_pdf(0, 0, std::ptr::null()), 0);
}
#[test]
fn test_mp_get_bookmarks_json_serialize_fail() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("empty.pdf");
std::fs::write(&path, b"not a pdf").unwrap();
let path_c = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let result = mp_get_bookmarks(path_c.as_ptr());
assert!(result.is_null());
}
}