Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/bugfixes/issue-1366/after.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/bugfixes/issue-1366/before.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/bugfixes/issue-1366/gt.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions crates/office2pdf/src/ir/elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1314,8 +1314,8 @@ impl ImageCrop {
pub struct ImageData {
pub data: Vec<u8>,
/// Clockwise rotation in degrees about the image's centre: the picture's
/// own `a:xfrm/@rot` composed with the angle of any rotated ancestor
/// `<p:grpSp>`. `None` means upright (issues #682, #895).
/// own `a:xfrm/@rot`, composed with the angle of any rotated ancestor
/// `<p:grpSp>` in PPTX. `None` means upright (issues #682, #895, #1366).
pub rotation_deg: Option<f64>,
/// Mirror a fixed PPTX picture left-to-right across its frame's vertical
/// axis. PPTX stores this as `a:xfrm/@flipH`; the frame is flipped after
Expand Down
53 changes: 53 additions & 0 deletions crates/office2pdf/src/parser/docx_image_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,59 @@ fn test_docx_floating_image_square_wrap() {
assert!((floating[0].image.height.expect("Expected height") - 100.0).abs() < 0.5);
}

#[test]
fn docx_floating_picture_rotation_reaches_ir_and_typst() {
let document_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<w:body>
<w:p><w:r><w:drawing>
<wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0"
relativeHeight="1" behindDoc="1" locked="0"
layoutInCell="1" allowOverlap="1">
<wp:simplePos x="0" y="0"/>
<wp:positionH relativeFrom="margin"><wp:posOffset>914400</wp:posOffset></wp:positionH>
<wp:positionV relativeFrom="paragraph"><wp:posOffset>457200</wp:posOffset></wp:positionV>
<wp:extent cx="4366365" cy="4366365"/>
<wp:wrapNone/>
<wp:docPr id="1" name="Picture 1"/>
<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic>
<pic:nvPicPr><pic:cNvPr id="1" name="image1.bmp"/><pic:cNvPicPr/></pic:nvPicPr>
<pic:blipFill><a:blip r:embed="rIdImage1"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
<pic:spPr>
<a:xfrm rot="20933656"><a:off x="0" y="0"/><a:ext cx="4366365" cy="4366365"/></a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</pic:spPr>
</pic:pic>
</a:graphicData></a:graphic>
</wp:anchor>
</w:drawing></w:r></w:p>
<w:sectPr/>
</w:body>
</w:document>"#;

let data = build_docx_with_custom_image_document(document_xml);
let parser = DocxParser;
let (doc, _warnings) = parser.parse(&data, &ConvertOptions::default()).unwrap();
let floating = find_floating_images(&doc);

assert_eq!(floating.len(), 1);
assert_eq!(floating[0].image.rotation_deg, Some(349.0));

let source = crate::render::typst_gen::generate_typst(&doc)
.unwrap()
.source;
assert!(
source.contains("#move(dx:")
&& source.contains("#rotate(349deg, origin: top + left)[#image("),
"floating picture rotation was not emitted: {source}"
);
}

#[test]
fn test_docx_floating_image_top_and_bottom_wrap() {
let bmp_data = make_test_bmp();
Expand Down
2 changes: 1 addition & 1 deletion crates/office2pdf/src/parser/docx_media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub(super) fn extract_drawing_image(
};

let image_data = ImageData {
rotation_deg: None,
rotation_deg: (pic.rot != 0).then_some(f64::from(pic.rot)),
flip_h: false,
flip_v: false,
data: asset.data.clone(),
Expand Down
86 changes: 56 additions & 30 deletions crates/office2pdf/src/render/typst_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3444,16 +3444,8 @@ fn generate_floating_image(out: &mut String, fi: &FloatingImage, ctx: &mut GenCt
" #place(top + left, dx: {}pt, dy: 0pt)[",
format_f64(fi.offset_x)
);
out.push_str("#image(\"");
out.push_str(&path);
out.push('"');
if let Some(w) = fi.image.width {
let _ = write!(out, ", width: {}pt", format_f64(w));
}
if let Some(h) = fi.image.height {
let _ = write!(out, ", height: {}pt", format_f64(h));
}
out.push_str(")]\n");
generate_floating_image_content(out, &fi.image, &path);
out.push_str("]\n");
// Reserve vertical space equal to image height
if let Some(h) = fi.image.height {
let _ = writeln!(out, " #v({}pt)", format_f64(h));
Expand All @@ -3468,16 +3460,8 @@ fn generate_floating_image(out: &mut String, fi: &FloatingImage, ctx: &mut GenCt
format_f64(fi.offset_x),
format_f64(fi.offset_y)
);
out.push_str("#image(\"");
out.push_str(&path);
out.push('"');
if let Some(w) = fi.image.width {
let _ = write!(out, ", width: {}pt", format_f64(w));
}
if let Some(h) = fi.image.height {
let _ = write!(out, ", height: {}pt", format_f64(h));
}
out.push_str(")]\n");
generate_floating_image_content(out, &fi.image, &path);
out.push_str("]\n");
}
WrapMode::Square | WrapMode::Tight => {
// Best-effort text wrapping: use #place with float: true
Expand All @@ -3487,18 +3471,60 @@ fn generate_floating_image(out: &mut String, fi: &FloatingImage, ctx: &mut GenCt
format_f64(fi.offset_x),
format_f64(fi.offset_y)
);
out.push_str("#image(\"");
out.push_str(&path);
out.push('"');
if let Some(w) = fi.image.width {
let _ = write!(out, ", width: {}pt", format_f64(w));
}
if let Some(h) = fi.image.height {
let _ = write!(out, ", height: {}pt", format_f64(h));
}
out.push_str(")]\n");
generate_floating_image_content(out, &fi.image, &path);
out.push_str("]\n");
}
}
}

/// Emit a floating picture while preserving its unrotated frame dimensions
/// and centre.
///
/// Word turns `a:xfrm/@rot` clockwise around the picture centre. The image is
/// already inside an absolute `#place`, so rotation changes only its painted
/// extent and does not affect document flow. As with oversized fixed elements,
/// Typst can clamp the body frame before resolving `origin: center`; pivot on
/// the unclamped top-left corner and translate it back to Word's centre instead
/// (issues #1032, #1366).
fn generate_floating_image_content(out: &mut String, image: &ImageData, path: &str) {
let rotation = image.rotation_deg.filter(|degrees| *degrees != 0.0);
let pivot_shift = rotation.and_then(|degrees| {
image
.width
.zip(image.height)
.map(|(width, height)| centre_pivot_shift(width, height, degrees, false, false))
});
if let Some(degrees) = rotation {
if let Some((dx, dy)) = pivot_shift {
let _ = write!(
out,
"#move(dx: {}pt, dy: {}pt)[#rotate({}deg, origin: top + left)[",
format_f64(dx),
format_f64(dy),
format_f64(degrees)
);
} else {
let _ = write!(out, "#rotate({}deg, origin: center)[", format_f64(degrees));
}
}

out.push_str("#image(\"");
out.push_str(path);
out.push('"');
if let Some(width) = image.width {
let _ = write!(out, ", width: {}pt", format_f64(width));
}
if let Some(height) = image.height {
let _ = write!(out, ", height: {}pt", format_f64(height));
}
out.push(')');

if rotation.is_some() {
out.push(']');
}
if pivot_shift.is_some() {
out.push(']');
}
}

fn generate_floating_text_box(
Expand Down
Loading