1pub mod config;
41pub mod error;
42pub mod ir;
43pub mod parser;
44pub mod render;
45#[cfg(feature = "wasm")]
46pub mod wasm;
47
48use config::{ConvertOptions, Format};
49use error::{ConvertError, ConvertResult};
50use parser::Parser;
51
52#[cfg(not(target_arch = "wasm32"))]
65pub fn convert(path: impl AsRef<std::path::Path>) -> Result<ConvertResult, ConvertError> {
66 convert_with_options(path, &ConvertOptions::default())
67}
68
69#[cfg(not(target_arch = "wasm32"))]
80pub fn convert_with_options(
81 path: impl AsRef<std::path::Path>,
82 options: &ConvertOptions,
83) -> Result<ConvertResult, ConvertError> {
84 let path = path.as_ref();
85 let ext = path
86 .extension()
87 .and_then(|e| e.to_str())
88 .ok_or_else(|| ConvertError::UnsupportedFormat("no file extension".to_string()))?;
89
90 let format = Format::from_extension(ext)
91 .ok_or_else(|| ConvertError::UnsupportedFormat(ext.to_string()))?;
92
93 let data = std::fs::read(path)?;
94 convert_bytes(&data, format, options)
95}
96
97pub fn convert_bytes(
106 data: &[u8],
107 format: Format,
108 options: &ConvertOptions,
109) -> Result<ConvertResult, ConvertError> {
110 let parser: Box<dyn Parser> = match format {
111 Format::Docx => Box::new(parser::docx::DocxParser),
112 Format::Pptx => Box::new(parser::pptx::PptxParser),
113 Format::Xlsx => Box::new(parser::xlsx::XlsxParser),
114 };
115
116 let (doc, warnings) = parser.parse(data, options)?;
117 let output = render::typst_gen::generate_typst_with_options(&doc, options)?;
118 let pdf = render::pdf::compile_to_pdf(
119 &output.source,
120 &output.images,
121 options.pdf_standard,
122 &options.font_paths,
123 )?;
124 Ok(ConvertResult { pdf, warnings })
125}
126
127pub fn render_document(doc: &ir::Document) -> Result<Vec<u8>, ConvertError> {
138 let output = render::typst_gen::generate_typst(doc)?;
139 render::pdf::compile_to_pdf(&output.source, &output.images, None, &[])
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use ir::*;
146
147 fn make_simple_document(text: &str) -> Document {
149 Document {
150 metadata: Metadata::default(),
151 pages: vec![Page::Flow(FlowPage {
152 size: PageSize::default(),
153 margins: Margins::default(),
154 content: vec![Block::Paragraph(Paragraph {
155 style: ParagraphStyle::default(),
156 runs: vec![Run {
157 text: text.to_string(),
158 style: TextStyle::default(),
159 href: None,
160 footnote: None,
161 }],
162 })],
163 header: None,
164 footer: None,
165 })],
166 styles: StyleSheet::default(),
167 }
168 }
169
170 #[test]
173 fn test_convert_unsupported_format() {
174 let result = convert("test.txt");
175 assert!(result.is_err());
176 let err = result.unwrap_err();
177 assert!(matches!(err, ConvertError::UnsupportedFormat(_)));
178 }
179
180 #[test]
181 fn test_convert_no_extension() {
182 let result = convert("test");
183 assert!(result.is_err());
184 assert!(matches!(
185 result.unwrap_err(),
186 ConvertError::UnsupportedFormat(_)
187 ));
188 }
189
190 #[test]
191 fn test_format_detection_all_supported_extensions() {
192 assert!(convert_bytes(b"fake", Format::Docx, &ConvertOptions::default()).is_err());
194 assert!(convert_bytes(b"fake", Format::Pptx, &ConvertOptions::default()).is_err());
195 assert!(convert_bytes(b"fake", Format::Xlsx, &ConvertOptions::default()).is_err());
196 }
197
198 #[test]
199 fn test_convert_bytes_propagates_parse_error() {
200 for format in [Format::Docx, Format::Pptx, Format::Xlsx] {
202 let result = convert_bytes(b"fake", format, &ConvertOptions::default());
203 assert!(result.is_err());
204 assert!(
205 matches!(result.unwrap_err(), ConvertError::Parse(_)),
206 "Expected Parse error for {format:?}"
207 );
208 }
209 }
210
211 #[test]
212 fn test_convert_nonexistent_file_returns_io_error() {
213 let result = convert("nonexistent_file.docx");
214 assert!(result.is_err());
215 assert!(matches!(result.unwrap_err(), ConvertError::Io(_)));
216 }
217
218 #[test]
221 fn test_render_document_empty_document() {
222 let doc = Document {
223 metadata: Metadata::default(),
224 pages: vec![],
225 styles: StyleSheet::default(),
226 };
227 let pdf = render_document(&doc).unwrap();
228 assert!(!pdf.is_empty(), "PDF bytes should not be empty");
229 assert!(pdf.starts_with(b"%PDF"), "Should be valid PDF");
230 }
231
232 #[test]
233 fn test_render_document_single_paragraph() {
234 let doc = make_simple_document("Hello, World!");
235 let pdf = render_document(&doc).unwrap();
236 assert!(!pdf.is_empty());
237 assert!(pdf.starts_with(b"%PDF"));
238 }
239
240 #[test]
241 fn test_render_document_styled_text() {
242 let doc = Document {
243 metadata: Metadata::default(),
244 pages: vec![Page::Flow(FlowPage {
245 size: PageSize::default(),
246 margins: Margins::default(),
247 content: vec![Block::Paragraph(Paragraph {
248 style: ParagraphStyle {
249 alignment: Some(Alignment::Center),
250 ..ParagraphStyle::default()
251 },
252 runs: vec![
253 Run {
254 text: "Bold text ".to_string(),
255 style: TextStyle {
256 bold: Some(true),
257 font_size: Some(16.0),
258 ..TextStyle::default()
259 },
260 href: None,
261 footnote: None,
262 },
263 Run {
264 text: "and italic".to_string(),
265 style: TextStyle {
266 italic: Some(true),
267 color: Some(Color::new(255, 0, 0)),
268 ..TextStyle::default()
269 },
270 href: None,
271 footnote: None,
272 },
273 ],
274 })],
275 header: None,
276 footer: None,
277 })],
278 styles: StyleSheet::default(),
279 };
280 let pdf = render_document(&doc).unwrap();
281 assert!(!pdf.is_empty());
282 assert!(pdf.starts_with(b"%PDF"));
283 }
284
285 #[test]
286 fn test_render_document_multiple_flow_pages() {
287 let doc = Document {
288 metadata: Metadata::default(),
289 pages: vec![
290 Page::Flow(FlowPage {
291 size: PageSize::default(),
292 margins: Margins::default(),
293 content: vec![Block::Paragraph(Paragraph {
294 style: ParagraphStyle::default(),
295 runs: vec![Run {
296 text: "Page 1".to_string(),
297 style: TextStyle::default(),
298 href: None,
299 footnote: None,
300 }],
301 })],
302 header: None,
303 footer: None,
304 }),
305 Page::Flow(FlowPage {
306 size: PageSize::default(),
307 margins: Margins::default(),
308 content: vec![Block::Paragraph(Paragraph {
309 style: ParagraphStyle::default(),
310 runs: vec![Run {
311 text: "Page 2".to_string(),
312 style: TextStyle::default(),
313 href: None,
314 footnote: None,
315 }],
316 })],
317 header: None,
318 footer: None,
319 }),
320 ],
321 styles: StyleSheet::default(),
322 };
323 let pdf = render_document(&doc).unwrap();
324 assert!(!pdf.is_empty());
325 assert!(pdf.starts_with(b"%PDF"));
326 }
327
328 #[test]
329 fn test_render_document_page_break() {
330 let doc = Document {
331 metadata: Metadata::default(),
332 pages: vec![Page::Flow(FlowPage {
333 size: PageSize::default(),
334 margins: Margins::default(),
335 content: vec![
336 Block::Paragraph(Paragraph {
337 style: ParagraphStyle::default(),
338 runs: vec![Run {
339 text: "Before break".to_string(),
340 style: TextStyle::default(),
341 href: None,
342 footnote: None,
343 }],
344 }),
345 Block::PageBreak,
346 Block::Paragraph(Paragraph {
347 style: ParagraphStyle::default(),
348 runs: vec![Run {
349 text: "After break".to_string(),
350 style: TextStyle::default(),
351 href: None,
352 footnote: None,
353 }],
354 }),
355 ],
356 header: None,
357 footer: None,
358 })],
359 styles: StyleSheet::default(),
360 };
361 let pdf = render_document(&doc).unwrap();
362 assert!(!pdf.is_empty());
363 assert!(pdf.starts_with(b"%PDF"));
364 }
365
366 #[test]
367 fn test_convert_with_options_delegates_to_convert_bytes() {
368 let result = convert_with_options("nonexistent.docx", &ConvertOptions::default());
371 assert!(matches!(result.unwrap_err(), ConvertError::Io(_)));
372 }
373
374 #[test]
375 fn test_convert_delegates_to_convert_with_options() {
376 let result = convert("nonexistent.docx");
378 assert!(matches!(result.unwrap_err(), ConvertError::Io(_)));
379 }
380
381 fn make_test_png() -> Vec<u8> {
385 fn png_crc32(chunk_type: &[u8], data: &[u8]) -> u32 {
387 let mut crc: u32 = 0xFFFF_FFFF;
388 for &byte in chunk_type.iter().chain(data.iter()) {
389 crc ^= byte as u32;
390 for _ in 0..8 {
391 if crc & 1 != 0 {
392 crc = (crc >> 1) ^ 0xEDB8_8320;
393 } else {
394 crc >>= 1;
395 }
396 }
397 }
398 crc ^ 0xFFFF_FFFF
399 }
400
401 let mut png = Vec::new();
402 png.extend_from_slice(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
403 let ihdr_data: [u8; 13] = [
404 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00,
405 ];
406 let ihdr_type = b"IHDR";
407 png.extend_from_slice(&(ihdr_data.len() as u32).to_be_bytes());
408 png.extend_from_slice(ihdr_type);
409 png.extend_from_slice(&ihdr_data);
410 png.extend_from_slice(&png_crc32(ihdr_type, &ihdr_data).to_be_bytes());
411 let idat_data: [u8; 15] = [
412 0x78, 0x01, 0x01, 0x04, 0x00, 0xFB, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x03, 0x01, 0x01,
413 0x00,
414 ];
415 let idat_type = b"IDAT";
416 png.extend_from_slice(&(idat_data.len() as u32).to_be_bytes());
417 png.extend_from_slice(idat_type);
418 png.extend_from_slice(&idat_data);
419 png.extend_from_slice(&png_crc32(idat_type, &idat_data).to_be_bytes());
420 let iend_type = b"IEND";
421 png.extend_from_slice(&0u32.to_be_bytes());
422 png.extend_from_slice(iend_type);
423 png.extend_from_slice(&png_crc32(iend_type, &[]).to_be_bytes());
424 png
425 }
426
427 #[test]
428 fn test_render_document_with_image() {
429 let doc = Document {
430 metadata: Metadata::default(),
431 pages: vec![Page::Flow(FlowPage {
432 size: PageSize::default(),
433 margins: Margins::default(),
434 content: vec![Block::Image(ImageData {
435 data: make_test_png(),
436 format: ImageFormat::Png,
437 width: Some(100.0),
438 height: Some(80.0),
439 })],
440 header: None,
441 footer: None,
442 })],
443 styles: StyleSheet::default(),
444 };
445 let pdf = render_document(&doc).unwrap();
446 assert!(!pdf.is_empty(), "PDF should not be empty");
447 assert!(pdf.starts_with(b"%PDF"), "Should be valid PDF");
448 }
449
450 #[test]
451 fn test_render_document_image_mixed_with_text() {
452 let doc = Document {
453 metadata: Metadata::default(),
454 pages: vec![Page::Flow(FlowPage {
455 size: PageSize::default(),
456 margins: Margins::default(),
457 content: vec![
458 Block::Paragraph(Paragraph {
459 style: ParagraphStyle::default(),
460 runs: vec![Run {
461 text: "Image below:".to_string(),
462 style: TextStyle::default(),
463 href: None,
464 footnote: None,
465 }],
466 }),
467 Block::Image(ImageData {
468 data: make_test_png(),
469 format: ImageFormat::Png,
470 width: Some(200.0),
471 height: None,
472 }),
473 Block::Paragraph(Paragraph {
474 style: ParagraphStyle::default(),
475 runs: vec![Run {
476 text: "Image above.".to_string(),
477 style: TextStyle::default(),
478 href: None,
479 footnote: None,
480 }],
481 }),
482 ],
483 header: None,
484 footer: None,
485 })],
486 styles: StyleSheet::default(),
487 };
488 let pdf = render_document(&doc).unwrap();
489 assert!(!pdf.is_empty());
490 assert!(pdf.starts_with(b"%PDF"));
491 }
492
493 fn build_test_docx() -> Vec<u8> {
497 use std::io::Cursor;
498 let docx = docx_rs::Docx::new()
499 .add_paragraph(
500 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Hello from DOCX")),
501 )
502 .add_paragraph(
503 docx_rs::Paragraph::new()
504 .add_run(docx_rs::Run::new().add_text("Second paragraph").bold()),
505 );
506 let mut cursor = Cursor::new(Vec::new());
507 docx.build().pack(&mut cursor).unwrap();
508 cursor.into_inner()
509 }
510
511 fn build_test_xlsx() -> Vec<u8> {
513 use std::io::Cursor;
514 let mut book = umya_spreadsheet::new_file();
515 {
516 let sheet = book.get_sheet_mut(&0).unwrap();
517 sheet.get_cell_mut("A1").set_value("Name");
518 sheet.get_cell_mut("B1").set_value("Value");
519 sheet.get_cell_mut("A2").set_value("Item 1");
520 sheet.get_cell_mut("B2").set_value("100");
521 }
522 let mut cursor = Cursor::new(Vec::new());
523 umya_spreadsheet::writer::xlsx::write_writer(&book, &mut cursor).unwrap();
524 cursor.into_inner()
525 }
526
527 fn build_test_pptx() -> Vec<u8> {
529 use std::io::{Cursor, Write};
530 let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
531 let opts = zip::write::FileOptions::default();
532
533 zip.start_file("[Content_Types].xml", opts).unwrap();
535 zip.write_all(
536 br#"<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/></Types>"#,
537 ).unwrap();
538
539 zip.start_file("_rels/.rels", opts).unwrap();
541 zip.write_all(
542 br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/></Relationships>"#,
543 ).unwrap();
544
545 zip.start_file("ppt/presentation.xml", opts).unwrap();
547 zip.write_all(
548 br#"<?xml version="1.0" encoding="UTF-8"?><p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:sldSz cx="9144000" cy="6858000"/><p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst></p:presentation>"#,
549 ).unwrap();
550
551 zip.start_file("ppt/_rels/presentation.xml.rels", opts)
553 .unwrap();
554 zip.write_all(
555 br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>"#,
556 ).unwrap();
557
558 zip.start_file("ppt/slides/slide1.xml", opts).unwrap();
560 zip.write_all(
561 br#"<?xml version="1.0" encoding="UTF-8"?><p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/><p:sp><p:nvSpPr><p:cNvPr id="2" name="TextBox 1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="457200" y="274638"/><a:ext cx="8229600" cy="1143000"/></a:xfrm></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>Hello from PPTX</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>"#,
562 ).unwrap();
563
564 zip.start_file("ppt/slides/_rels/slide1.xml.rels", opts)
566 .unwrap();
567 zip.write_all(
568 br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>"#,
569 ).unwrap();
570
571 zip.finish().unwrap().into_inner()
572 }
573
574 #[test]
575 fn test_e2e_docx_to_pdf() {
576 let docx_bytes = build_test_docx();
577 let result = convert_bytes(&docx_bytes, Format::Docx, &ConvertOptions::default()).unwrap();
578 assert!(
579 !result.pdf.is_empty(),
580 "DOCX→PDF should produce non-empty output"
581 );
582 assert!(
583 result.pdf.starts_with(b"%PDF"),
584 "Output should be valid PDF"
585 );
586 assert!(
587 result.warnings.is_empty(),
588 "Normal DOCX should produce no warnings"
589 );
590 }
591
592 #[test]
593 fn test_e2e_xlsx_to_pdf() {
594 let xlsx_bytes = build_test_xlsx();
595 let result = convert_bytes(&xlsx_bytes, Format::Xlsx, &ConvertOptions::default()).unwrap();
596 assert!(
597 !result.pdf.is_empty(),
598 "XLSX→PDF should produce non-empty output"
599 );
600 assert!(
601 result.pdf.starts_with(b"%PDF"),
602 "Output should be valid PDF"
603 );
604 }
605
606 #[test]
607 fn test_e2e_pptx_to_pdf() {
608 let pptx_bytes = build_test_pptx();
609 let result = convert_bytes(&pptx_bytes, Format::Pptx, &ConvertOptions::default()).unwrap();
610 assert!(
611 !result.pdf.is_empty(),
612 "PPTX→PDF should produce non-empty output"
613 );
614 assert!(
615 result.pdf.starts_with(b"%PDF"),
616 "Output should be valid PDF"
617 );
618 }
619
620 #[test]
621 fn test_e2e_docx_with_table_to_pdf() {
622 use std::io::Cursor;
623 let table = docx_rs::Table::new(vec![docx_rs::TableRow::new(vec![
624 docx_rs::TableCell::new().add_paragraph(
625 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Cell A")),
626 ),
627 docx_rs::TableCell::new().add_paragraph(
628 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Cell B")),
629 ),
630 ])]);
631 let docx = docx_rs::Docx::new()
632 .add_paragraph(
633 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Table below:")),
634 )
635 .add_table(table);
636 let mut cursor = Cursor::new(Vec::new());
637 docx.build().pack(&mut cursor).unwrap();
638 let data = cursor.into_inner();
639
640 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
641 assert!(!result.pdf.is_empty());
642 assert!(result.pdf.starts_with(b"%PDF"));
643 }
644
645 #[test]
646 fn test_e2e_convert_with_options_from_temp_file() {
647 let docx_bytes = build_test_docx();
648 let dir = std::env::temp_dir();
649 let input = dir.join("office2pdf_test_input.docx");
650 let output = dir.join("office2pdf_test_output.pdf");
651 std::fs::write(&input, &docx_bytes).unwrap();
652
653 let result = convert(&input).unwrap();
654 assert!(!result.pdf.is_empty());
655 assert!(result.pdf.starts_with(b"%PDF"));
656
657 let result2 = convert_with_options(&input, &ConvertOptions::default()).unwrap();
659 assert!(!result2.pdf.is_empty());
660 assert!(result2.pdf.starts_with(b"%PDF"));
661
662 std::fs::write(&output, &result.pdf).unwrap();
664 assert!(output.exists());
665 let written = std::fs::read(&output).unwrap();
666 assert!(written.starts_with(b"%PDF"));
667
668 let _ = std::fs::remove_file(&input);
670 let _ = std::fs::remove_file(&output);
671 }
672
673 #[test]
674 fn test_e2e_unsupported_format_error_message() {
675 let result = convert("document.odt");
676 let err = result.unwrap_err();
677 match err {
678 ConvertError::UnsupportedFormat(ref ext) => {
679 assert_eq!(ext, "odt", "Error should mention the unsupported extension");
680 }
681 _ => panic!("Expected UnsupportedFormat error, got {err:?}"),
682 }
683 }
684
685 #[test]
686 fn test_e2e_missing_file_error() {
687 let result = convert("nonexistent_document.docx");
688 assert!(
689 matches!(result.unwrap_err(), ConvertError::Io(_)),
690 "Missing file should produce IO error"
691 );
692 }
693
694 #[test]
697 fn test_render_document_with_system_font_in_ir() {
698 let doc = Document {
702 metadata: Metadata::default(),
703 pages: vec![Page::Flow(FlowPage {
704 size: PageSize::default(),
705 margins: Margins::default(),
706 content: vec![Block::Paragraph(Paragraph {
707 style: ParagraphStyle::default(),
708 runs: vec![Run {
709 text: "Hello with system font".to_string(),
710 style: TextStyle {
711 font_family: Some("Arial".to_string()),
712 ..TextStyle::default()
713 },
714 href: None,
715 footnote: None,
716 }],
717 })],
718 header: None,
719 footer: None,
720 })],
721 styles: StyleSheet::default(),
722 };
723 let pdf = render_document(&doc).unwrap();
724 assert!(!pdf.is_empty());
725 assert!(pdf.starts_with(b"%PDF"));
726 }
727
728 #[test]
729 fn test_render_document_with_multiple_font_families() {
730 let doc = Document {
732 metadata: Metadata::default(),
733 pages: vec![Page::Flow(FlowPage {
734 size: PageSize::default(),
735 margins: Margins::default(),
736 content: vec![Block::Paragraph(Paragraph {
737 style: ParagraphStyle::default(),
738 runs: vec![
739 Run {
740 text: "Calibri text ".to_string(),
741 style: TextStyle {
742 font_family: Some("Calibri".to_string()),
743 ..TextStyle::default()
744 },
745 href: None,
746 footnote: None,
747 },
748 Run {
749 text: "and Times New Roman text".to_string(),
750 style: TextStyle {
751 font_family: Some("Times New Roman".to_string()),
752 ..TextStyle::default()
753 },
754 href: None,
755 footnote: None,
756 },
757 ],
758 })],
759 header: None,
760 footer: None,
761 })],
762 styles: StyleSheet::default(),
763 };
764 let pdf = render_document(&doc).unwrap();
765 assert!(!pdf.is_empty());
766 assert!(pdf.starts_with(b"%PDF"));
767 }
768
769 fn build_pptx_with_broken_slide() -> Vec<u8> {
774 use std::io::{Cursor, Write};
775 let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
776 let opts = zip::write::FileOptions::default();
777
778 zip.start_file("[Content_Types].xml", opts).unwrap();
779 zip.write_all(
780 br#"<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/><Override PartName="/ppt/slides/slide2.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/></Types>"#,
781 ).unwrap();
782
783 zip.start_file("_rels/.rels", opts).unwrap();
784 zip.write_all(
785 br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/></Relationships>"#,
786 ).unwrap();
787
788 zip.start_file("ppt/presentation.xml", opts).unwrap();
790 zip.write_all(
791 br#"<?xml version="1.0" encoding="UTF-8"?><p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:sldSz cx="9144000" cy="6858000"/><p:sldIdLst><p:sldId id="256" r:id="rId2"/><p:sldId id="257" r:id="rId3"/></p:sldIdLst></p:presentation>"#,
792 ).unwrap();
793
794 zip.start_file("ppt/_rels/presentation.xml.rels", opts)
795 .unwrap();
796 zip.write_all(
797 br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/></Relationships>"#,
798 ).unwrap();
799
800 zip.start_file("ppt/slides/slide1.xml", opts).unwrap();
802 zip.write_all(
803 br#"<?xml version="1.0" encoding="UTF-8"?><p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/><p:sp><p:nvSpPr><p:cNvPr id="2" name="TextBox 1"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="457200" y="274638"/><a:ext cx="8229600" cy="1143000"/></a:xfrm></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>Valid slide content</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld></p:sld>"#,
804 ).unwrap();
805
806 zip.start_file("ppt/slides/_rels/slide1.xml.rels", opts)
807 .unwrap();
808 zip.write_all(
809 br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>"#,
810 ).unwrap();
811
812 zip.finish().unwrap().into_inner()
817 }
818
819 #[test]
820 fn test_convert_result_has_pdf_and_warnings() {
821 let docx_bytes = build_test_docx();
822 let result = convert_bytes(&docx_bytes, Format::Docx, &ConvertOptions::default()).unwrap();
823 assert!(result.pdf.starts_with(b"%PDF"));
825 let _warnings: &Vec<error::ConvertWarning> = &result.warnings;
826 }
827
828 #[test]
829 fn test_pptx_broken_slide_emits_warning_and_produces_pdf() {
830 let pptx_bytes = build_pptx_with_broken_slide();
831 let result = convert_bytes(&pptx_bytes, Format::Pptx, &ConvertOptions::default()).unwrap();
832
833 assert!(
835 !result.pdf.is_empty(),
836 "Should produce PDF despite broken slide"
837 );
838 assert!(
839 result.pdf.starts_with(b"%PDF"),
840 "Output should be valid PDF"
841 );
842
843 assert!(
845 !result.warnings.is_empty(),
846 "Should emit warning for broken slide"
847 );
848 let warning_text = result.warnings[0].to_string();
850 assert!(
851 warning_text.contains("slide") || warning_text.contains("Slide"),
852 "Warning should mention the problematic slide: {warning_text}"
853 );
854 }
855
856 #[test]
857 fn test_render_document_with_list() {
858 use crate::ir::{
859 Document, FlowPage, List, ListItem, ListKind, Margins, Metadata, Page, PageSize,
860 Paragraph, ParagraphStyle, Run, StyleSheet, TextStyle,
861 };
862 let doc = Document {
863 metadata: Metadata::default(),
864 pages: vec![Page::Flow(FlowPage {
865 size: PageSize::default(),
866 margins: Margins::default(),
867 content: vec![ir::Block::List(List {
868 kind: ListKind::Unordered,
869 items: vec![
870 ListItem {
871 content: vec![Paragraph {
872 style: ParagraphStyle::default(),
873 runs: vec![Run {
874 text: "Hello".to_string(),
875 style: TextStyle::default(),
876 href: None,
877 footnote: None,
878 }],
879 }],
880 level: 0,
881 },
882 ListItem {
883 content: vec![Paragraph {
884 style: ParagraphStyle::default(),
885 runs: vec![Run {
886 text: "World".to_string(),
887 style: TextStyle::default(),
888 href: None,
889 footnote: None,
890 }],
891 }],
892 level: 0,
893 },
894 ],
895 })],
896 header: None,
897 footer: None,
898 })],
899 styles: StyleSheet::default(),
900 };
901 let pdf = render_document(&doc).unwrap();
902 assert!(
903 pdf.starts_with(b"%PDF"),
904 "Should produce valid PDF with list"
905 );
906 }
907
908 #[test]
909 fn test_e2e_docx_with_list_produces_pdf() {
910 use std::io::Cursor;
911 let abstract_num = docx_rs::AbstractNumbering::new(0).add_level(docx_rs::Level::new(
913 0,
914 docx_rs::Start::new(1),
915 docx_rs::NumberFormat::new("bullet"),
916 docx_rs::LevelText::new("•"),
917 docx_rs::LevelJc::new("left"),
918 ));
919 let numbering = docx_rs::Numbering::new(1, 0);
920 let nums = docx_rs::Numberings::new()
921 .add_abstract_numbering(abstract_num)
922 .add_numbering(numbering);
923
924 let docx = docx_rs::Docx::new()
925 .numberings(nums)
926 .add_paragraph(
927 docx_rs::Paragraph::new()
928 .add_run(docx_rs::Run::new().add_text("Bullet 1"))
929 .numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
930 )
931 .add_paragraph(
932 docx_rs::Paragraph::new()
933 .add_run(docx_rs::Run::new().add_text("Bullet 2"))
934 .numbering(docx_rs::NumberingId::new(1), docx_rs::IndentLevel::new(0)),
935 )
936 .add_paragraph(
937 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Regular text")),
938 );
939
940 let mut cursor = Cursor::new(Vec::new());
941 docx.build().pack(&mut cursor).unwrap();
942 let data = cursor.into_inner();
943
944 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
945 assert!(
946 result.pdf.starts_with(b"%PDF"),
947 "Should produce valid PDF with list content"
948 );
949 }
950
951 #[test]
952 fn test_normal_docx_has_no_warnings() {
953 let docx_bytes = build_test_docx();
954 let result = convert_bytes(&docx_bytes, Format::Docx, &ConvertOptions::default()).unwrap();
955 assert!(
956 result.warnings.is_empty(),
957 "Normal DOCX should produce no warnings"
958 );
959 }
960
961 #[test]
964 fn test_render_document_with_header() {
965 let doc = Document {
966 metadata: Metadata::default(),
967 pages: vec![Page::Flow(FlowPage {
968 size: PageSize::default(),
969 margins: Margins::default(),
970 content: vec![Block::Paragraph(Paragraph {
971 style: ParagraphStyle::default(),
972 runs: vec![Run {
973 text: "Body content".to_string(),
974 style: TextStyle::default(),
975 href: None,
976 footnote: None,
977 }],
978 })],
979 header: Some(ir::HeaderFooter {
980 paragraphs: vec![ir::HeaderFooterParagraph {
981 style: ParagraphStyle::default(),
982 elements: vec![ir::HFInline::Run(Run {
983 text: "My Header".to_string(),
984 style: TextStyle::default(),
985 href: None,
986 footnote: None,
987 })],
988 }],
989 }),
990 footer: None,
991 })],
992 styles: StyleSheet::default(),
993 };
994 let pdf = render_document(&doc).unwrap();
995 assert!(!pdf.is_empty());
996 assert!(pdf.starts_with(b"%PDF"));
997 }
998
999 #[test]
1000 fn test_render_document_with_page_number_footer() {
1001 let doc = Document {
1002 metadata: Metadata::default(),
1003 pages: vec![Page::Flow(FlowPage {
1004 size: PageSize::default(),
1005 margins: Margins::default(),
1006 content: vec![Block::Paragraph(Paragraph {
1007 style: ParagraphStyle::default(),
1008 runs: vec![Run {
1009 text: "Body content".to_string(),
1010 style: TextStyle::default(),
1011 href: None,
1012 footnote: None,
1013 }],
1014 })],
1015 header: None,
1016 footer: Some(ir::HeaderFooter {
1017 paragraphs: vec![ir::HeaderFooterParagraph {
1018 style: ParagraphStyle::default(),
1019 elements: vec![
1020 ir::HFInline::Run(Run {
1021 text: "Page ".to_string(),
1022 style: TextStyle::default(),
1023 href: None,
1024 footnote: None,
1025 }),
1026 ir::HFInline::PageNumber,
1027 ],
1028 }],
1029 }),
1030 })],
1031 styles: StyleSheet::default(),
1032 };
1033 let pdf = render_document(&doc).unwrap();
1034 assert!(!pdf.is_empty());
1035 assert!(pdf.starts_with(b"%PDF"));
1036 }
1037
1038 #[test]
1039 fn test_e2e_docx_with_header_footer_to_pdf() {
1040 use std::io::Cursor;
1041 let header = docx_rs::Header::new().add_paragraph(
1042 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Document Title")),
1043 );
1044 let footer = docx_rs::Footer::new().add_paragraph(
1045 docx_rs::Paragraph::new().add_run(
1046 docx_rs::Run::new()
1047 .add_text("Page ")
1048 .add_field_char(docx_rs::FieldCharType::Begin, false)
1049 .add_instr_text(docx_rs::InstrText::PAGE(docx_rs::InstrPAGE::new()))
1050 .add_field_char(docx_rs::FieldCharType::Separate, false)
1051 .add_text("1")
1052 .add_field_char(docx_rs::FieldCharType::End, false),
1053 ),
1054 );
1055 let docx = docx_rs::Docx::new()
1056 .header(header)
1057 .footer(footer)
1058 .add_paragraph(
1059 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Body paragraph")),
1060 );
1061 let mut cursor = Cursor::new(Vec::new());
1062 docx.build().pack(&mut cursor).unwrap();
1063 let data = cursor.into_inner();
1064
1065 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
1066 assert!(
1067 result.pdf.starts_with(b"%PDF"),
1068 "DOCX with header/footer should produce valid PDF"
1069 );
1070 }
1071
1072 #[test]
1075 fn test_render_document_with_landscape_page() {
1076 let doc = Document {
1078 metadata: Metadata::default(),
1079 pages: vec![Page::Flow(FlowPage {
1080 size: PageSize {
1081 width: 841.9, height: 595.3,
1083 },
1084 margins: Margins::default(),
1085 content: vec![Block::Paragraph(Paragraph {
1086 runs: vec![Run {
1087 text: "Landscape page".to_string(),
1088 style: TextStyle::default(),
1089 href: None,
1090 footnote: None,
1091 }],
1092 style: ParagraphStyle::default(),
1093 })],
1094 header: None,
1095 footer: None,
1096 })],
1097 styles: StyleSheet::default(),
1098 };
1099 let pdf = render_document(&doc).unwrap();
1100 assert!(
1101 !pdf.is_empty(),
1102 "Landscape FlowPage should produce non-empty PDF"
1103 );
1104 assert!(pdf.starts_with(b"%PDF"), "Should produce valid PDF");
1105 }
1106
1107 #[test]
1108 fn test_e2e_landscape_docx_to_pdf() {
1109 use std::io::Cursor;
1110 let docx = docx_rs::Docx::new()
1112 .page_size(16838, 11906)
1113 .page_orient(docx_rs::PageOrientationType::Landscape)
1114 .page_margin(
1115 docx_rs::PageMargin::new()
1116 .top(1440)
1117 .bottom(1440)
1118 .left(1440)
1119 .right(1440),
1120 )
1121 .add_paragraph(
1122 docx_rs::Paragraph::new()
1123 .add_run(docx_rs::Run::new().add_text("Landscape document")),
1124 );
1125 let mut cursor = Cursor::new(Vec::new());
1126 docx.build().pack(&mut cursor).unwrap();
1127 let data = cursor.into_inner();
1128
1129 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
1130 assert!(
1131 result.pdf.starts_with(b"%PDF"),
1132 "Landscape DOCX should produce valid PDF"
1133 );
1134 }
1135
1136 #[test]
1137 fn test_docx_toc_pipeline_produces_pdf() {
1138 use std::io::Cursor;
1139 let toc = docx_rs::TableOfContents::new()
1140 .heading_styles_range(1, 3)
1141 .alias("Table of contents")
1142 .add_item(
1143 docx_rs::TableOfContentsItem::new()
1144 .text("Chapter 1")
1145 .toc_key("_Toc00000001")
1146 .level(1)
1147 .page_ref("2"),
1148 )
1149 .add_item(
1150 docx_rs::TableOfContentsItem::new()
1151 .text("Chapter 2")
1152 .toc_key("_Toc00000002")
1153 .level(1)
1154 .page_ref("5"),
1155 );
1156
1157 let docx = docx_rs::Docx::new()
1158 .add_style(
1159 docx_rs::Style::new("Heading1", docx_rs::StyleType::Paragraph).name("Heading 1"),
1160 )
1161 .add_table_of_contents(toc)
1162 .add_paragraph(
1163 docx_rs::Paragraph::new()
1164 .add_run(docx_rs::Run::new().add_text("Chapter 1"))
1165 .style("Heading1"),
1166 )
1167 .add_paragraph(
1168 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Some body text")),
1169 );
1170
1171 let mut cursor = Cursor::new(Vec::new());
1172 docx.build().pack(&mut cursor).unwrap();
1173 let data = cursor.into_inner();
1174
1175 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
1176 assert!(
1177 result.pdf.starts_with(b"%PDF"),
1178 "DOCX with TOC should produce valid PDF"
1179 );
1180 }
1181
1182 #[test]
1183 fn test_convert_bytes_with_pdfa_option() {
1184 use std::io::Cursor;
1185 let docx = docx_rs::Docx::new().add_paragraph(
1186 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("PDF/A test")),
1187 );
1188 let mut cursor = Cursor::new(Vec::new());
1189 docx.build().pack(&mut cursor).unwrap();
1190 let data = cursor.into_inner();
1191
1192 let options = ConvertOptions {
1193 pdf_standard: Some(config::PdfStandard::PdfA2b),
1194 ..Default::default()
1195 };
1196 let result = convert_bytes(&data, Format::Docx, &options).unwrap();
1197 assert!(result.pdf.starts_with(b"%PDF"));
1198 let pdf_str = String::from_utf8_lossy(&result.pdf);
1200 assert!(
1201 pdf_str.contains("pdfaid") || pdf_str.contains("PDF/A"),
1202 "PDF/A conversion should include PDF/A metadata"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_render_document_default_no_pdfa() {
1208 let doc = make_simple_document("No PDF/A");
1209 let pdf = render_document(&doc).unwrap();
1210 let pdf_str = String::from_utf8_lossy(&pdf);
1211 assert!(
1212 !pdf_str.contains("pdfaid:conformance"),
1213 "Default render_document should not produce PDF/A"
1214 );
1215 }
1216
1217 #[test]
1218 fn test_convert_bytes_with_paper_size_override() {
1219 use std::io::Cursor;
1220 let docx = docx_rs::Docx::new().add_paragraph(
1221 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Paper size test")),
1222 );
1223 let mut cursor = Cursor::new(Vec::new());
1224 docx.build().pack(&mut cursor).unwrap();
1225 let data = cursor.into_inner();
1226
1227 let options = ConvertOptions {
1228 paper_size: Some(config::PaperSize::Letter),
1229 ..Default::default()
1230 };
1231 let result = convert_bytes(&data, Format::Docx, &options).unwrap();
1232 assert!(
1233 result.pdf.starts_with(b"%PDF"),
1234 "DOCX with Letter paper override should produce valid PDF"
1235 );
1236 }
1237
1238 #[test]
1239 fn test_convert_bytes_with_landscape_override() {
1240 use std::io::Cursor;
1241 let docx = docx_rs::Docx::new().add_paragraph(
1242 docx_rs::Paragraph::new()
1243 .add_run(docx_rs::Run::new().add_text("Landscape override test")),
1244 );
1245 let mut cursor = Cursor::new(Vec::new());
1246 docx.build().pack(&mut cursor).unwrap();
1247 let data = cursor.into_inner();
1248
1249 let options = ConvertOptions {
1250 landscape: Some(true),
1251 ..Default::default()
1252 };
1253 let result = convert_bytes(&data, Format::Docx, &options).unwrap();
1254 assert!(
1255 result.pdf.starts_with(b"%PDF"),
1256 "DOCX with landscape override should produce valid PDF"
1257 );
1258 }
1259
1260 #[test]
1263 fn test_edge_empty_docx_produces_valid_pdf() {
1264 use std::io::Cursor;
1265 let docx = docx_rs::Docx::new();
1266 let mut cursor = Cursor::new(Vec::new());
1267 docx.build().pack(&mut cursor).unwrap();
1268 let data = cursor.into_inner();
1269 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
1270 assert!(
1271 result.pdf.starts_with(b"%PDF"),
1272 "Empty DOCX should produce valid PDF"
1273 );
1274 }
1275
1276 #[test]
1277 fn test_edge_empty_xlsx_produces_valid_pdf() {
1278 use std::io::Cursor;
1279 let book = umya_spreadsheet::new_file();
1280 let mut cursor = Cursor::new(Vec::new());
1281 umya_spreadsheet::writer::xlsx::write_writer(&book, &mut cursor).unwrap();
1282 let data = cursor.into_inner();
1283 let result = convert_bytes(&data, Format::Xlsx, &ConvertOptions::default()).unwrap();
1284 assert!(
1285 result.pdf.starts_with(b"%PDF"),
1286 "Empty XLSX should produce valid PDF"
1287 );
1288 }
1289
1290 #[test]
1291 fn test_edge_empty_pptx_produces_valid_pdf() {
1292 use std::io::{Cursor, Write};
1293 let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
1295 let opts = zip::write::FileOptions::default();
1296 zip.start_file("[Content_Types].xml", opts).unwrap();
1297 zip.write_all(
1298 br#"<?xml version="1.0" encoding="UTF-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/></Types>"#
1299 ).unwrap();
1300 zip.start_file("_rels/.rels", opts).unwrap();
1301 zip.write_all(
1302 br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/></Relationships>"#,
1303 ).unwrap();
1304 zip.start_file("ppt/presentation.xml", opts).unwrap();
1305 zip.write_all(
1306 br#"<?xml version="1.0" encoding="UTF-8"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldSz cx="9144000" cy="6858000"/><p:sldIdLst/></p:presentation>"#,
1307 ).unwrap();
1308 zip.start_file("ppt/_rels/presentation.xml.rels", opts)
1309 .unwrap();
1310 zip.write_all(
1311 br#"<?xml version="1.0" encoding="UTF-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>"#,
1312 ).unwrap();
1313 let data = zip.finish().unwrap().into_inner();
1314 let result = convert_bytes(&data, Format::Pptx, &ConvertOptions::default()).unwrap();
1315 assert!(
1316 result.pdf.starts_with(b"%PDF"),
1317 "Empty PPTX should produce valid PDF"
1318 );
1319 }
1320
1321 #[test]
1322 fn test_edge_long_paragraph_no_panic() {
1323 use std::io::Cursor;
1324 let long_text: String = "Lorem ipsum dolor sit amet. ".repeat(400);
1326 let docx = docx_rs::Docx::new().add_paragraph(
1327 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text(&long_text)),
1328 );
1329 let mut cursor = Cursor::new(Vec::new());
1330 docx.build().pack(&mut cursor).unwrap();
1331 let data = cursor.into_inner();
1332 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
1333 assert!(
1334 result.pdf.starts_with(b"%PDF"),
1335 "Long paragraph should produce valid PDF"
1336 );
1337 }
1338
1339 #[test]
1340 fn test_edge_large_table_no_panic() {
1341 use std::io::Cursor;
1342 let mut book = umya_spreadsheet::new_file();
1343 {
1344 let sheet = book.get_sheet_mut(&0).unwrap();
1345 for row in 1..=100u32 {
1347 for col in 1..=20u32 {
1348 let coord = format!("{}{}", (b'A' + ((col - 1) % 26) as u8) as char, row);
1349 sheet
1350 .get_cell_mut(coord.as_str())
1351 .set_value(format!("R{row}C{col}"));
1352 }
1353 }
1354 }
1355 let mut cursor = Cursor::new(Vec::new());
1356 umya_spreadsheet::writer::xlsx::write_writer(&book, &mut cursor).unwrap();
1357 let data = cursor.into_inner();
1358 let result = convert_bytes(&data, Format::Xlsx, &ConvertOptions::default()).unwrap();
1359 assert!(
1360 result.pdf.starts_with(b"%PDF"),
1361 "Large table should produce valid PDF"
1362 );
1363 }
1364
1365 #[test]
1366 fn test_edge_corrupted_docx_returns_error() {
1367 let data = b"not a valid ZIP file at all";
1368 let result = convert_bytes(data, Format::Docx, &ConvertOptions::default());
1369 assert!(result.is_err(), "Corrupted DOCX should return an error");
1370 let err = result.unwrap_err();
1371 match err {
1372 ConvertError::Parse(msg) => {
1373 assert!(!msg.is_empty(), "Error message should not be empty");
1374 }
1375 _ => panic!("Expected Parse error for corrupted DOCX, got {err:?}"),
1376 }
1377 }
1378
1379 #[test]
1380 fn test_edge_corrupted_xlsx_returns_error() {
1381 let data = b"this is not an xlsx file";
1382 let result = convert_bytes(data, Format::Xlsx, &ConvertOptions::default());
1383 assert!(result.is_err(), "Corrupted XLSX should return an error");
1384 }
1385
1386 #[test]
1387 fn test_edge_corrupted_pptx_returns_error() {
1388 let data = b"garbage data that is not a pptx";
1389 let result = convert_bytes(data, Format::Pptx, &ConvertOptions::default());
1390 assert!(result.is_err(), "Corrupted PPTX should return an error");
1391 }
1392
1393 #[test]
1394 fn test_edge_truncated_zip_returns_error() {
1395 let full_data = build_test_docx();
1397 let truncated = &full_data[..full_data.len() / 2];
1398 let result = convert_bytes(truncated, Format::Docx, &ConvertOptions::default());
1399 assert!(result.is_err(), "Truncated DOCX should return an error");
1400 }
1401
1402 #[test]
1403 fn test_edge_unicode_cjk_text() {
1404 use std::io::Cursor;
1405 let docx = docx_rs::Docx::new().add_paragraph(
1406 docx_rs::Paragraph::new()
1407 .add_run(docx_rs::Run::new().add_text("中文测试 日本語テスト 한국어 테스트")),
1408 );
1409 let mut cursor = Cursor::new(Vec::new());
1410 docx.build().pack(&mut cursor).unwrap();
1411 let data = cursor.into_inner();
1412 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
1413 assert!(
1414 result.pdf.starts_with(b"%PDF"),
1415 "CJK text should produce valid PDF"
1416 );
1417 }
1418
1419 #[test]
1420 fn test_edge_unicode_emoji_text() {
1421 use std::io::Cursor;
1422 let docx = docx_rs::Docx::new().add_paragraph(
1423 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("Hello 🌍🎉💡 World")),
1424 );
1425 let mut cursor = Cursor::new(Vec::new());
1426 docx.build().pack(&mut cursor).unwrap();
1427 let data = cursor.into_inner();
1428 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
1430 assert!(
1431 result.pdf.starts_with(b"%PDF"),
1432 "Emoji text should produce valid PDF"
1433 );
1434 }
1435
1436 #[test]
1437 fn test_edge_unicode_rtl_text() {
1438 use std::io::Cursor;
1439 let docx = docx_rs::Docx::new().add_paragraph(
1440 docx_rs::Paragraph::new().add_run(docx_rs::Run::new().add_text("مرحبا بالعالم")), );
1442 let mut cursor = Cursor::new(Vec::new());
1443 docx.build().pack(&mut cursor).unwrap();
1444 let data = cursor.into_inner();
1445 let result = convert_bytes(&data, Format::Docx, &ConvertOptions::default()).unwrap();
1446 assert!(
1447 result.pdf.starts_with(b"%PDF"),
1448 "RTL text should produce valid PDF"
1449 );
1450 }
1451
1452 #[test]
1453 fn test_edge_image_only_docx() {
1454 let doc = Document {
1456 metadata: Metadata::default(),
1457 pages: vec![Page::Flow(FlowPage {
1458 size: PageSize::default(),
1459 margins: Margins::default(),
1460 content: vec![Block::Image(ImageData {
1461 data: vec![0x89, 0x50, 0x4E, 0x47], format: ir::ImageFormat::Png,
1463 width: Some(100.0),
1464 height: Some(100.0),
1465 })],
1466 header: None,
1467 footer: None,
1468 })],
1469 styles: StyleSheet::default(),
1470 };
1471 let _result = render_document(&doc);
1474 }
1475}