Skip to content

Commit 937d844

Browse files
Merge pull request #172 from developer0hye/fix/pptx-fixed-textbox-layout
fix: improve pptx slide fidelity
2 parents 8c72a0b + 793e8e4 commit 937d844

20 files changed

Lines changed: 2269 additions & 63 deletions

crates/office2pdf/src/ir/elements.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ pub struct TextBoxData {
152152
/// When true, text should not wrap — the content width is unconstrained.
153153
/// Corresponds to `<a:bodyPr wrap="none"/>` in OOXML.
154154
pub no_wrap: bool,
155+
/// Whether the source requested PowerPoint autofit behavior for this box.
156+
pub auto_fit: bool,
155157
}
156158

157159
/// The kind of list: ordered (numbered) or unordered (bulleted).

crates/office2pdf/src/lib_render_tests.rs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::test_support::{make_simple_document, make_test_png};
22
use super::*;
33
use crate::ir::*;
4+
use std::collections::BTreeMap;
45

56
#[test]
67
fn test_render_document_empty_document() {
@@ -259,6 +260,140 @@ fn test_render_document_image_mixed_with_text() {
259260
assert!(pdf.starts_with(b"%PDF"));
260261
}
261262

263+
#[test]
264+
fn test_render_document_fixed_textbox_ordered_list_keeps_all_numbers() {
265+
let doc = Document {
266+
metadata: Metadata::default(),
267+
pages: vec![Page::Fixed(FixedPage {
268+
size: PageSize {
269+
width: 780.0,
270+
height: 540.0,
271+
},
272+
elements: vec![FixedElement {
273+
x: 300.0,
274+
y: 200.0,
275+
width: 260.0,
276+
height: 160.0,
277+
kind: FixedElementKind::TextBox(TextBoxData {
278+
content: vec![Block::List(List {
279+
kind: ListKind::Ordered,
280+
items: vec![
281+
ListItem {
282+
content: vec![Paragraph {
283+
style: ParagraphStyle {
284+
indent_left: Some(36.0),
285+
indent_first_line: Some(-36.0),
286+
..ParagraphStyle::default()
287+
},
288+
runs: vec![Run {
289+
text: "Alpha".to_string(),
290+
style: TextStyle {
291+
font_size: Some(20.0),
292+
..TextStyle::default()
293+
},
294+
href: None,
295+
footnote: None,
296+
}],
297+
}],
298+
level: 0,
299+
start_at: Some(1),
300+
},
301+
ListItem {
302+
content: vec![Paragraph {
303+
style: ParagraphStyle {
304+
indent_left: Some(36.0),
305+
indent_first_line: Some(-36.0),
306+
..ParagraphStyle::default()
307+
},
308+
runs: vec![Run {
309+
text: "Beta".to_string(),
310+
style: TextStyle {
311+
font_size: Some(20.0),
312+
..TextStyle::default()
313+
},
314+
href: None,
315+
footnote: None,
316+
}],
317+
}],
318+
level: 0,
319+
start_at: None,
320+
},
321+
ListItem {
322+
content: vec![Paragraph {
323+
style: ParagraphStyle {
324+
indent_left: Some(36.0),
325+
indent_first_line: Some(-36.0),
326+
..ParagraphStyle::default()
327+
},
328+
runs: vec![Run {
329+
text: "Gamma".to_string(),
330+
style: TextStyle {
331+
font_size: Some(20.0),
332+
..TextStyle::default()
333+
},
334+
href: None,
335+
footnote: None,
336+
}],
337+
}],
338+
level: 0,
339+
start_at: None,
340+
},
341+
],
342+
level_styles: BTreeMap::from([(
343+
0,
344+
ListLevelStyle {
345+
kind: ListKind::Ordered,
346+
numbering_pattern: Some("1.".to_string()),
347+
full_numbering: false,
348+
marker_text: None,
349+
marker_style: None,
350+
},
351+
)]),
352+
})],
353+
padding: Insets::default(),
354+
vertical_align: TextBoxVerticalAlign::Top,
355+
fill: None,
356+
opacity: None,
357+
stroke: None,
358+
shape_kind: None,
359+
no_wrap: false,
360+
auto_fit: false,
361+
}),
362+
}],
363+
background_color: None,
364+
background_gradient: None,
365+
})],
366+
styles: StyleSheet::default(),
367+
};
368+
369+
let pdf = render_document(&doc).unwrap();
370+
let text = pdf_extract::extract_text_from_mem(&pdf).unwrap();
371+
assert!(
372+
text.contains("1."),
373+
"Expected first marker in PDF text, got:\n{text}",
374+
);
375+
assert!(
376+
text.contains("2."),
377+
"Expected second marker in PDF text, got:\n{text}",
378+
);
379+
assert!(
380+
text.contains("3."),
381+
"Expected third marker in PDF text, got:\n{text}",
382+
);
383+
assert!(
384+
text.contains("Alpha"),
385+
"Expected first item text, got:\n{text}"
386+
);
387+
assert!(
388+
text.contains("Beta"),
389+
"Expected second item text, got:\n{text}"
390+
);
391+
assert!(
392+
text.contains("Gamma"),
393+
"Expected third item text, got:\n{text}"
394+
);
395+
}
396+
262397
#[test]
263398
fn test_render_document_with_system_font_in_ir() {
264399
let doc = Document {
@@ -585,6 +720,7 @@ fn test_render_pptx_style_document_size() {
585720
stroke: None,
586721
shape_kind: None,
587722
no_wrap: false,
723+
auto_fit: false,
588724
}),
589725
}],
590726
}));
@@ -644,6 +780,7 @@ fn test_render_document_with_centered_fixed_text_box() {
644780
stroke: None,
645781
shape_kind: None,
646782
no_wrap: false,
783+
auto_fit: false,
647784
}),
648785
}],
649786
})],
@@ -656,3 +793,69 @@ fn test_render_document_with_centered_fixed_text_box() {
656793
"Centered fixed text box should compile to a valid PDF"
657794
);
658795
}
796+
797+
#[test]
798+
fn test_render_document_with_auto_fit_fixed_text_box() {
799+
let doc = Document {
800+
metadata: Metadata::default(),
801+
pages: vec![Page::Fixed(FixedPage {
802+
size: PageSize {
803+
width: 300.0,
804+
height: 200.0,
805+
},
806+
background_color: None,
807+
background_gradient: None,
808+
elements: vec![FixedElement {
809+
x: 20.0,
810+
y: 20.0,
811+
width: 150.0,
812+
height: 22.0,
813+
kind: FixedElementKind::TextBox(TextBoxData {
814+
content: vec![Block::Paragraph(Paragraph {
815+
style: ParagraphStyle {
816+
alignment: Some(Alignment::Right),
817+
..ParagraphStyle::default()
818+
},
819+
runs: vec![
820+
Run {
821+
text: "3. 시스템 연동 방안 ".to_string(),
822+
style: TextStyle {
823+
font_size: Some(28.0),
824+
bold: Some(true),
825+
..TextStyle::default()
826+
},
827+
href: None,
828+
footnote: None,
829+
},
830+
Run {
831+
text: "클라우드 기반 업무 시스템 연동".to_string(),
832+
style: TextStyle {
833+
font_size: Some(16.0),
834+
bold: Some(true),
835+
..TextStyle::default()
836+
},
837+
href: None,
838+
footnote: None,
839+
},
840+
],
841+
})],
842+
padding: Insets::default(),
843+
vertical_align: TextBoxVerticalAlign::Top,
844+
fill: None,
845+
opacity: None,
846+
stroke: None,
847+
shape_kind: None,
848+
no_wrap: false,
849+
auto_fit: true,
850+
}),
851+
}],
852+
})],
853+
styles: StyleSheet::default(),
854+
};
855+
856+
let pdf = render_document(&doc).unwrap();
857+
assert!(
858+
pdf.starts_with(b"%PDF"),
859+
"Auto-fit fixed text box should compile to a valid PDF"
860+
);
861+
}

crates/office2pdf/src/parser/pptx.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ use self::theme::{
4040
resolve_effective_color_map, resolve_theme_font,
4141
};
4242

43+
#[path = "pptx_emf.rs"]
44+
mod emf;
4345
#[path = "pptx_package.rs"]
4446
mod package;
4547
#[path = "pptx_shapes.rs"]

0 commit comments

Comments
 (0)