use crate::ffi::document::Document;
use crate::ffi::{DOCUMENTS, Handle, HandleStore, STREAMS};
use std::collections::HashMap;
use std::ffi::{CStr, CString, c_char};
use std::ptr;
use std::sync::LazyLock;
type ContextHandle = Handle;
type StreamHandle = Handle;
type DeviceHandle = Handle;
type ArchiveHandle = Handle;
pub const XPS_CONTENT_FIXED_DOC_SEQ: i32 = 0;
pub const XPS_CONTENT_FIXED_DOC: i32 = 1;
pub const XPS_CONTENT_FIXED_PAGE: i32 = 2;
pub const XPS_CONTENT_FONT: i32 = 3;
pub const XPS_CONTENT_IMAGE: i32 = 4;
pub const XPS_CONTENT_ICC_PROFILE: i32 = 5;
pub const XPS_CONTENT_RESOURCE_DICT: i32 = 6;
pub const XPS_CONTENT_PRINT_TICKET: i32 = 7;
pub const XPS_CONTENT_THUMBNAIL: i32 = 8;
pub const XPS_REL_CORE_PROPERTIES: i32 = 0;
pub const XPS_REL_DIGITAL_SIGNATURE: i32 = 1;
pub const XPS_REL_THUMBNAIL: i32 = 2;
pub const XPS_REL_PRINT_TICKET: i32 = 3;
pub const XPS_REL_RESTRICTED_FONT: i32 = 4;
pub const XPS_REL_REQUIRED_RESOURCE: i32 = 5;
#[derive(Debug, Clone)]
pub struct XpsPart {
pub name: String,
pub data: Vec<u8>,
pub content_type: String,
}
impl XpsPart {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
data: Vec::new(),
content_type: String::new(),
}
}
pub fn with_data(mut self, data: &[u8]) -> Self {
self.data = data.to_vec();
self
}
pub fn with_content_type(mut self, content_type: &str) -> Self {
self.content_type = content_type.to_string();
self
}
}
#[derive(Debug, Clone)]
pub struct XpsFixedPage {
pub name: String,
pub number: i32,
pub width: f32,
pub height: f32,
pub content: String,
}
impl XpsFixedPage {
pub fn new(name: &str, number: i32) -> Self {
Self {
name: name.to_string(),
number,
width: 816.0, height: 1056.0, content: String::new(),
}
}
pub fn with_size(mut self, width: f32, height: f32) -> Self {
self.width = width;
self.height = height;
self
}
}
#[derive(Debug, Clone)]
pub struct XpsFixedDocument {
pub name: String,
pub outline: Option<String>,
pub pages: Vec<XpsFixedPage>,
}
impl XpsFixedDocument {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
outline: None,
pages: Vec::new(),
}
}
pub fn add_page(&mut self, page: XpsFixedPage) {
self.pages.push(page);
}
}
#[derive(Debug, Clone)]
pub struct XpsResource {
pub key: String,
pub uri: String,
pub data: Vec<u8>,
}
impl XpsResource {
pub fn new(key: &str, uri: &str) -> Self {
Self {
key: key.to_string(),
uri: uri.to_string(),
data: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct XpsFontEntry {
pub name: String,
pub data: Vec<u8>,
pub obfuscated: bool,
}
pub struct XpsDocument {
pub context: ContextHandle,
pub start_part: String,
pub documents: Vec<XpsFixedDocument>,
pub parts: HashMap<String, XpsPart>,
pub fonts: HashMap<String, XpsFontEntry>,
pub resources: HashMap<String, Vec<XpsResource>>,
pub targets: HashMap<String, i32>,
pub page_count: i32,
}
impl XpsDocument {
pub fn new(context: ContextHandle) -> Self {
Self {
context,
start_part: String::new(),
documents: Vec::new(),
parts: HashMap::new(),
fonts: HashMap::new(),
resources: HashMap::new(),
targets: HashMap::new(),
page_count: 0,
}
}
pub fn add_document(&mut self, doc: XpsFixedDocument) {
self.page_count += doc.pages.len() as i32;
self.documents.push(doc);
}
pub fn add_part(&mut self, part: XpsPart) {
self.parts.insert(part.name.clone(), part);
}
pub fn get_part(&self, name: &str) -> Option<&XpsPart> {
self.parts.get(name)
}
pub fn has_part(&self, name: &str) -> bool {
self.parts.contains_key(name)
}
pub fn get_page(&self, index: i32) -> Option<&XpsFixedPage> {
let mut current = 0;
for doc in &self.documents {
for page in &doc.pages {
if current == index {
return Some(page);
}
current += 1;
}
}
None
}
}
pub static XPS_DOCUMENTS: LazyLock<HandleStore<XpsDocument>> = LazyLock::new(HandleStore::new);
#[unsafe(no_mangle)]
pub extern "C" fn xps_new_document(ctx: ContextHandle) -> Handle {
let doc = XpsDocument::new(ctx);
XPS_DOCUMENTS.insert(doc)
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_drop_document(_ctx: ContextHandle, doc: Handle) {
XPS_DOCUMENTS.remove(doc);
}
fn count_xps_pages(data: &[u8]) -> i32 {
if let Ok(archive) = crate::fitz::archive::Archive::from_buffer(data.to_vec()) {
let names = archive.entry_names();
let count = names
.iter()
.filter(|name| {
let lower = name.to_lowercase();
lower.ends_with(".fpage")
})
.count();
return count.max(1) as i32;
}
1
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_open_document(_ctx: ContextHandle, filename: *const c_char) -> Handle {
if filename.is_null() {
return 0;
}
let c_str = unsafe { CStr::from_ptr(filename) };
let path = match c_str.to_str() {
Ok(s) => s,
Err(_) => return 0,
};
match std::fs::read(path) {
Ok(data) => {
if data.is_empty() {
return 0;
}
if data.len() < 2 || data[0] != 0x50 || data[1] != 0x4B {
return 0;
}
let page_count = count_xps_pages(&data);
let mut doc = Document::new(data);
doc.format = "xps".to_string();
doc.page_count = page_count;
DOCUMENTS.insert(doc)
}
Err(_) => 0,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_open_document_with_stream(
_ctx: ContextHandle,
stream: StreamHandle,
) -> Handle {
if let Some(stream_arc) = STREAMS.get(stream) {
if let Ok(guard) = stream_arc.lock() {
let data = guard.data.clone();
if data.is_empty() {
return 0;
}
if data.len() >= 2 && data[0] == 0x50 && data[1] == 0x4B {
let page_count = count_xps_pages(&data);
let mut doc = Document::new(data);
doc.format = "xps".to_string();
doc.page_count = page_count;
return DOCUMENTS.insert(doc);
}
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_open_document_with_directory(
_ctx: ContextHandle,
archive: ArchiveHandle,
) -> Handle {
if let Some(archive_arc) = super::archive::ARCHIVES.get(archive) {
if let Ok(guard) = archive_arc.lock() {
if let Some(data) = guard.raw_data() {
if !data.is_empty() && data.len() >= 2 && data[0] == 0x50 && data[1] == 0x4B {
let page_count = count_xps_pages(&data);
let mut doc = Document::new(data);
doc.format = "xps".to_string();
doc.page_count = page_count;
return DOCUMENTS.insert(doc);
}
}
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_count_pages(_ctx: ContextHandle, doc: Handle) -> i32 {
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
return d.page_count;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_get_page_size(
_ctx: ContextHandle,
doc: Handle,
page_num: i32,
width: *mut f32,
height: *mut f32,
) -> i32 {
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
if let Some(page) = d.get_page(page_num) {
if !width.is_null() {
unsafe {
*width = page.width;
}
}
if !height.is_null() {
unsafe {
*height = page.height;
}
}
return 1;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_get_page_name(
_ctx: ContextHandle,
doc: Handle,
page_num: i32,
) -> *mut c_char {
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
if let Some(page) = d.get_page(page_num) {
if let Ok(cstr) = CString::new(page.name.clone()) {
return cstr.into_raw();
}
}
}
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_count_documents(_ctx: ContextHandle, doc: Handle) -> i32 {
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
return d.documents.len() as i32;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_get_document_name(
_ctx: ContextHandle,
doc: Handle,
doc_num: i32,
) -> *mut c_char {
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
if let Some(fixed_doc) = d.documents.get(doc_num as usize) {
if let Ok(cstr) = CString::new(fixed_doc.name.clone()) {
return cstr.into_raw();
}
}
}
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_count_pages_in_document(
_ctx: ContextHandle,
doc: Handle,
doc_num: i32,
) -> i32 {
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
if let Some(fixed_doc) = d.documents.get(doc_num as usize) {
return fixed_doc.pages.len() as i32;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_has_part(_ctx: ContextHandle, doc: Handle, name: *const c_char) -> i32 {
if name.is_null() {
return 0;
}
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
let part_name = unsafe { CStr::from_ptr(name).to_string_lossy() };
return if d.has_part(&part_name) { 1 } else { 0 };
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_get_part_data(
_ctx: ContextHandle,
doc: Handle,
name: *const c_char,
len_out: *mut usize,
) -> *const u8 {
if name.is_null() {
return ptr::null();
}
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
let part_name = unsafe { CStr::from_ptr(name).to_string_lossy() };
if let Some(part) = d.get_part(&part_name) {
if !len_out.is_null() {
unsafe {
*len_out = part.data.len();
}
}
return part.data.as_ptr();
}
}
if !len_out.is_null() {
unsafe {
*len_out = 0;
}
}
ptr::null()
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_get_part_content_type(
_ctx: ContextHandle,
doc: Handle,
name: *const c_char,
) -> *mut c_char {
if name.is_null() {
return ptr::null_mut();
}
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
let part_name = unsafe { CStr::from_ptr(name).to_string_lossy() };
if let Some(part) = d.get_part(&part_name) {
if let Ok(cstr) = CString::new(part.content_type.clone()) {
return cstr.into_raw();
}
}
}
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_add_part(
_ctx: ContextHandle,
doc: Handle,
name: *const c_char,
data: *const u8,
len: usize,
content_type: *const c_char,
) -> i32 {
if name.is_null() || data.is_null() {
return 0;
}
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let mut d = d.lock().unwrap();
let part_name = unsafe { CStr::from_ptr(name).to_string_lossy().to_string() };
let part_data = unsafe { std::slice::from_raw_parts(data, len) };
let ct = if !content_type.is_null() {
unsafe { CStr::from_ptr(content_type).to_string_lossy().to_string() }
} else {
String::new()
};
let part = XpsPart::new(&part_name)
.with_data(part_data)
.with_content_type(&ct);
d.add_part(part);
return 1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_lookup_font(_ctx: ContextHandle, doc: Handle, uri: *const c_char) -> i32 {
if uri.is_null() {
return 0;
}
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
let font_uri = unsafe { CStr::from_ptr(uri).to_string_lossy() };
return if d.fonts.contains_key(font_uri.as_ref()) {
1
} else {
0
};
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_font_count(_ctx: ContextHandle, doc: Handle) -> i32 {
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
return d.fonts.len() as i32;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_add_target(
_ctx: ContextHandle,
doc: Handle,
name: *const c_char,
page: i32,
) -> i32 {
if name.is_null() {
return 0;
}
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let mut d = d.lock().unwrap();
let target_name = unsafe { CStr::from_ptr(name).to_string_lossy().to_string() };
d.targets.insert(target_name, page);
return 1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_lookup_target(_ctx: ContextHandle, doc: Handle, name: *const c_char) -> i32 {
if name.is_null() {
return -1;
}
if let Some(d) = XPS_DOCUMENTS.get(doc) {
let d = d.lock().unwrap();
let target_name = unsafe { CStr::from_ptr(name).to_string_lossy() };
if let Some(&page) = d.targets.get(target_name.as_ref()) {
return page;
}
}
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_resolve_url(
_ctx: ContextHandle,
base_uri: *const c_char,
path: *const c_char,
output: *mut c_char,
output_size: i32,
) -> i32 {
if path.is_null() || output.is_null() || output_size <= 0 {
return 0;
}
let base = if !base_uri.is_null() {
unsafe { CStr::from_ptr(base_uri).to_string_lossy().to_string() }
} else {
String::new()
};
let rel_path = unsafe { CStr::from_ptr(path).to_string_lossy() };
let resolved = if rel_path.starts_with('/') {
rel_path.to_string()
} else if base.is_empty() {
format!("/{}", rel_path)
} else {
let base_dir = base.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
format!("{}/{}", base_dir, rel_path)
};
let bytes = resolved.as_bytes();
let copy_len = bytes.len().min((output_size - 1) as usize);
unsafe {
ptr::copy_nonoverlapping(bytes.as_ptr(), output as *mut u8, copy_len);
*output.add(copy_len) = 0;
}
1
}
#[unsafe(no_mangle)]
pub extern "C" fn xps_content_type_string(_ctx: ContextHandle, content_type: i32) -> *mut c_char {
let s = match content_type {
XPS_CONTENT_FIXED_DOC_SEQ => "application/vnd.ms-package.xps-fixeddocumentsequence+xml",
XPS_CONTENT_FIXED_DOC => "application/vnd.ms-package.xps-fixeddocument+xml",
XPS_CONTENT_FIXED_PAGE => "application/vnd.ms-package.xps-fixedpage+xml",
XPS_CONTENT_FONT => "application/vnd.ms-opentype",
XPS_CONTENT_IMAGE => "image/png",
XPS_CONTENT_ICC_PROFILE => "application/vnd.ms-color.iccprofile",
XPS_CONTENT_RESOURCE_DICT => "application/vnd.ms-package.xps-resourcedictionary+xml",
XPS_CONTENT_PRINT_TICKET => "application/vnd.ms-printing.printticket+xml",
XPS_CONTENT_THUMBNAIL => "image/png",
_ => "application/octet-stream",
};
if let Ok(cstr) = CString::new(s) {
return cstr.into_raw();
}
ptr::null_mut()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_type_constants() {
assert_eq!(XPS_CONTENT_FIXED_DOC_SEQ, 0);
assert_eq!(XPS_CONTENT_FIXED_PAGE, 2);
assert_eq!(XPS_CONTENT_FONT, 3);
}
#[test]
fn test_xps_part() {
let part = XpsPart::new("/Documents/1/Pages/1.fpage")
.with_data(b"<FixedPage/>")
.with_content_type("application/vnd.ms-package.xps-fixedpage+xml");
assert_eq!(part.name, "/Documents/1/Pages/1.fpage");
assert_eq!(part.data, b"<FixedPage/>");
}
#[test]
fn test_xps_fixed_page() {
let page = XpsFixedPage::new("/Documents/1/Pages/1.fpage", 0).with_size(816.0, 1056.0);
assert_eq!(page.number, 0);
assert_eq!(page.width, 816.0);
assert_eq!(page.height, 1056.0);
}
#[test]
fn test_xps_fixed_document() {
let mut doc = XpsFixedDocument::new("/Documents/1/FixedDocument.fdoc");
doc.add_page(XpsFixedPage::new("/Documents/1/Pages/1.fpage", 0));
doc.add_page(XpsFixedPage::new("/Documents/1/Pages/2.fpage", 1));
assert_eq!(doc.pages.len(), 2);
}
#[test]
fn test_xps_document() {
let mut doc = XpsDocument::new(0);
let mut fixed_doc = XpsFixedDocument::new("/Documents/1/FixedDocument.fdoc");
fixed_doc.add_page(XpsFixedPage::new("/Documents/1/Pages/1.fpage", 0));
fixed_doc.add_page(XpsFixedPage::new("/Documents/1/Pages/2.fpage", 1));
doc.add_document(fixed_doc);
assert_eq!(doc.page_count, 2);
assert!(doc.get_page(0).is_some());
assert!(doc.get_page(1).is_some());
assert!(doc.get_page(2).is_none());
}
#[test]
fn test_ffi_document() {
let ctx = 0;
let doc = xps_new_document(ctx);
assert!(doc > 0);
assert_eq!(xps_count_pages(ctx, doc), 0);
assert_eq!(xps_count_documents(ctx, doc), 0);
xps_drop_document(ctx, doc);
}
#[test]
fn test_ffi_parts() {
let ctx = 0;
let doc = xps_new_document(ctx);
let name = CString::new("/test.xml").unwrap();
let data = b"<test/>";
let ct = CString::new("application/xml").unwrap();
assert_eq!(xps_has_part(ctx, doc, name.as_ptr()), 0);
let result = xps_add_part(
ctx,
doc,
name.as_ptr(),
data.as_ptr(),
data.len(),
ct.as_ptr(),
);
assert_eq!(result, 1);
assert_eq!(xps_has_part(ctx, doc, name.as_ptr()), 1);
xps_drop_document(ctx, doc);
}
#[test]
fn test_ffi_targets() {
let ctx = 0;
let doc = xps_new_document(ctx);
let name = CString::new("section1").unwrap();
assert_eq!(xps_lookup_target(ctx, doc, name.as_ptr()), -1);
xps_add_target(ctx, doc, name.as_ptr(), 5);
assert_eq!(xps_lookup_target(ctx, doc, name.as_ptr()), 5);
xps_drop_document(ctx, doc);
}
#[test]
fn test_ffi_content_type_string() {
let ctx = 0;
let s = xps_content_type_string(ctx, XPS_CONTENT_FIXED_PAGE);
assert!(!s.is_null());
unsafe {
let str = CStr::from_ptr(s).to_string_lossy();
assert!(str.contains("fixedpage"));
xps_free_string(s);
}
}
#[test]
fn test_ffi_resolve_url() {
let ctx = 0;
let mut output = [0u8; 256];
let base = CString::new("/Documents/1/Pages/1.fpage").unwrap();
let path = CString::new("../Resources/image.png").unwrap();
let result = xps_resolve_url(
ctx,
base.as_ptr(),
path.as_ptr(),
output.as_mut_ptr() as *mut c_char,
256,
);
assert_eq!(result, 1);
}
}