Skip to content

Commit a132a92

Browse files
committed
fix: preserve fixed textbox title and list layout
Signed-off-by: Yonghye Kwon <developer.0hye@gmail.com>
1 parent 8c72a0b commit a132a92

5 files changed

Lines changed: 511 additions & 52 deletions

File tree

crates/office2pdf/src/lib_render_tests.rs

Lines changed: 125 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,130 @@ 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+
}),
361+
}],
362+
background_color: None,
363+
background_gradient: None,
364+
})],
365+
styles: StyleSheet::default(),
366+
};
367+
368+
let pdf = render_document(&doc).unwrap();
369+
let text = pdf_extract::extract_text_from_mem(&pdf).unwrap();
370+
assert!(
371+
text.contains("1."),
372+
"Expected first marker in PDF text, got:\n{text}",
373+
);
374+
assert!(
375+
text.contains("2."),
376+
"Expected second marker in PDF text, got:\n{text}",
377+
);
378+
assert!(
379+
text.contains("3."),
380+
"Expected third marker in PDF text, got:\n{text}",
381+
);
382+
assert!(text.contains("Alpha"), "Expected first item text, got:\n{text}");
383+
assert!(text.contains("Beta"), "Expected second item text, got:\n{text}");
384+
assert!(text.contains("Gamma"), "Expected third item text, got:\n{text}");
385+
}
386+
262387
#[test]
263388
fn test_render_document_with_system_font_in_ir() {
264389
let doc = Document {

crates/office2pdf/src/render/typst_gen.rs

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -615,42 +615,19 @@ fn generate_fixed_text_box(
615615
&text_box.stroke,
616616
);
617617
}
618-
if text_box.no_wrap {
619-
// For wrap="none" text boxes: measure the natural content width first,
620-
// then use the larger of (measured width, original width) so that text
621-
// with slightly wider substitute fonts does not wrap.
622-
let _ = writeln!(out, " #let text_box_content_{text_box_id} = context {{");
623-
let _ = writeln!(out, " let _nowrap_draft = [");
624-
for (index, block) in text_box.content.iter().enumerate() {
625-
if index > 0 {
626-
out.push('\n');
627-
}
628-
out.push_str(" ");
629-
generate_fixed_text_box_block(out, block, ctx, Some(inner_width_pt), false)?;
630-
}
631-
let _ = writeln!(out, " ]");
632-
let _ = writeln!(
633-
out,
634-
" let _nowrap_w = calc.max(measure(_nowrap_draft).width, {}pt)",
635-
format_f64(inner_width_pt),
636-
);
637-
let _ = writeln!(out, " block(width: _nowrap_w, _nowrap_draft)");
638-
let _ = writeln!(out, " }}");
639-
} else {
640-
let _ = writeln!(
641-
out,
642-
" #let text_box_content_{text_box_id} = block(width: {}pt)[",
643-
format_f64(inner_width_pt),
644-
);
645-
for (index, block) in text_box.content.iter().enumerate() {
646-
if index > 0 {
647-
out.push('\n');
648-
}
649-
out.push_str(" ");
650-
generate_fixed_text_box_block(out, block, ctx, Some(inner_width_pt), false)?;
618+
let _ = writeln!(
619+
out,
620+
" #let text_box_content_{text_box_id} = block(width: {}pt)[",
621+
format_f64(inner_width_pt),
622+
);
623+
for (index, block) in text_box.content.iter().enumerate() {
624+
if index > 0 {
625+
out.push('\n');
651626
}
652-
out.push_str(" ]\n");
627+
out.push_str(" ");
628+
generate_fixed_text_box_block(out, block, ctx, Some(inner_width_pt), text_box.no_wrap)?;
653629
}
630+
out.push_str(" ]\n");
654631

655632
match text_box.vertical_align {
656633
TextBoxVerticalAlign::Top => {
@@ -1096,7 +1073,7 @@ fn generate_fixed_text_box_block(
10961073
fn generate_fixed_text_paragraph(
10971074
out: &mut String,
10981075
para: &Paragraph,
1099-
_no_wrap: bool,
1076+
no_wrap: bool,
11001077
) -> Result<(), ConvertError> {
11011078
let style: &ParagraphStyle = &para.style;
11021079
let needs_text_scope: bool = common_text_style(&para.runs).is_some();
@@ -1129,7 +1106,15 @@ fn generate_fixed_text_paragraph(
11291106
let _ = writeln!(out, "#block(width: 100%)[#set align({align_str})");
11301107
}
11311108

1132-
generate_runs_with_tabs(out, &para.runs, style.tab_stops.as_deref());
1109+
if no_wrap {
1110+
out.push_str("#box[");
1111+
generate_runs_with_tabs_no_wrap(out, &para.runs, style.tab_stops.as_deref());
1112+
} else {
1113+
generate_runs_with_tabs(out, &para.runs, style.tab_stops.as_deref());
1114+
}
1115+
if no_wrap {
1116+
out.push(']');
1117+
}
11331118

11341119
if use_align {
11351120
out.push(']');

0 commit comments

Comments
 (0)