use std::io::Write;
use dxpdf::render::layout::draw_command::{DrawCommand, LayoutedPage};
fn make_docx(document_xml: &str) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let o = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("[Content_Types].xml", o).unwrap();
zip.write_all(
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="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>"#,
)
.unwrap();
zip.start_file("_rels/.rels", o).unwrap();
zip.write_all(
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="word/document.xml"/>
</Relationships>"#,
)
.unwrap();
zip.start_file("word/document.xml", o).unwrap();
zip.write_all(document_xml.as_bytes()).unwrap();
zip.finish().unwrap();
}
buf
}
fn shape_document(body_pr_children: &str) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<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:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<w:body>
<w:p><w:r><w:drawing>
<wp:anchor distT="0" distB="0" distL="0" distR="0" simplePos="0"
relativeHeight="1" behindDoc="0" locked="0" layoutInCell="1" allowOverlap="1">
<wp:simplePos x="0" y="0"/>
<wp:positionH relativeFrom="margin"><wp:posOffset>0</wp:posOffset></wp:positionH>
<wp:positionV relativeFrom="paragraph"><wp:posOffset>0</wp:posOffset></wp:positionV>
<wp:extent cx="2540000" cy="1524000"/>
<wp:wrapNone/>
<wp:docPr id="1" name="Box"/>
<a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
<wps:wsp>
<wps:cNvSpPr/>
<wps:spPr>
<a:xfrm><a:off x="0" y="0"/><a:ext cx="2540000" cy="1524000"/></a:xfrm>
<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
</wps:spPr>
<wps:txbx><w:txbxContent>
<w:p><w:r><w:rPr><w:sz w:val="40"/></w:rPr><w:t>One</w:t></w:r></w:p>
<w:p><w:r><w:rPr><w:sz w:val="40"/></w:rPr><w:t>Two</w:t></w:r></w:p>
</w:txbxContent></wps:txbx>
<wps:bodyPr rot="0" vert="horz" wrap="square" vertOverflow="overflow"
lIns="0" tIns="0" rIns="0" bIns="0" anchor="t">{body_pr_children}</wps:bodyPr>
</wps:wsp>
</a:graphicData></a:graphic>
</wp:anchor>
</w:drawing></w:r></w:p>
</w:body>
</w:document>"#
)
}
fn layout(body_pr_children: &str) -> Vec<LayoutedPage> {
let xml = shape_document(body_pr_children);
let doc = dxpdf::docx::parse(&make_docx(&xml)).expect("parse");
dxpdf::render::resolve_and_layout(doc).1
}
fn body_text(pages: &[LayoutedPage]) -> Vec<(f32, f32)> {
pages
.iter()
.flat_map(|p| p.commands.iter())
.filter_map(|c| match c {
DrawCommand::Text {
text,
font_size,
position,
..
} if text.trim() == "One" || text.trim() == "Two" => {
Some((font_size.raw(), position.y.raw()))
}
_ => None,
})
.collect()
}
fn font_sizes(body_pr_children: &str) -> Vec<f32> {
body_text(&layout(body_pr_children))
.into_iter()
.map(|(s, _)| s)
.collect()
}
fn line_advance(body_pr_children: &str) -> f32 {
let t = body_text(&layout(body_pr_children));
assert_eq!(t.len(), 2, "fixture must emit exactly two lines: {t:?}");
t[1].1 - t[0].1
}
#[test]
fn a_bare_norm_autofit_leaves_the_body_unscaled() {
assert_eq!(font_sizes("<a:normAutofit/>"), vec![20.0, 20.0]);
}
#[test]
fn no_body_pr_autofit_at_all_leaves_the_body_unscaled() {
assert_eq!(font_sizes(""), vec![20.0, 20.0]);
}
#[test]
fn font_scale_shrinks_every_run_in_the_body() {
assert_eq!(
font_sizes(r#"<a:normAutofit fontScale="62500"/>"#),
vec![12.5, 12.5],
"the scale is a property of the body, so it reaches every run in it"
);
}
#[test]
fn no_autofit_does_not_shrink() {
assert_eq!(font_sizes("<a:noAutofit/>"), vec![20.0, 20.0]);
}
#[test]
fn sp_auto_fit_renders_unscaled() {
assert_eq!(font_sizes("<a:spAutoFit/>"), vec![20.0, 20.0]);
}
#[test]
fn line_spacing_reduction_tightens_the_body_without_resizing_it() {
let plain = line_advance("<a:normAutofit/>");
let tight = line_advance(r#"<a:normAutofit lnSpcReduction="20000"/>"#);
assert!(plain > 0.0, "fixture must stack its two paragraphs");
assert!(
(tight / plain - 0.8).abs() < 0.001,
"advance should fall to 80%: {plain} -> {tight}",
);
assert_eq!(
font_sizes(r#"<a:normAutofit lnSpcReduction="20000"/>"#),
vec![20.0, 20.0],
"lnSpcReduction must not touch glyph size",
);
}
#[test]
fn font_scale_and_line_spacing_reduction_compose() {
let both = r#"<a:normAutofit fontScale="62500" lnSpcReduction="20000"/>"#;
assert_eq!(font_sizes(both), vec![12.5, 12.5]);
let scaled_only = line_advance(r#"<a:normAutofit fontScale="62500"/>"#);
assert!(
(line_advance(both) / scaled_only - 0.8).abs() < 0.001,
"reduction applies on top of the font scale",
);
}
#[test]
fn an_explicit_full_scale_is_the_identity() {
assert_eq!(
font_sizes(r#"<a:normAutofit fontScale="100000" lnSpcReduction="0"/>"#),
vec![20.0, 20.0]
);
let plain = line_advance("<a:normAutofit/>");
let explicit = line_advance(r#"<a:normAutofit fontScale="100000" lnSpcReduction="0"/>"#);
assert!((explicit - plain).abs() < 0.001);
}
#[test]
fn the_scale_does_not_escape_the_shape() {
let xml = shape_document(r#"<a:normAutofit fontScale="62500"/>"#).replace(
"</w:body>",
r#"<w:p><w:r><w:rPr><w:sz w:val="40"/></w:rPr><w:t>Outside</w:t></w:r></w:p></w:body>"#,
);
let doc = dxpdf::docx::parse(&make_docx(&xml)).expect("parse");
let pages = dxpdf::render::resolve_and_layout(doc).1;
let outside: Vec<f32> = pages
.iter()
.flat_map(|p| p.commands.iter())
.filter_map(|c| match c {
DrawCommand::Text {
text, font_size, ..
} if text.trim() == "Outside" => Some(font_size.raw()),
_ => None,
})
.collect();
assert_eq!(outside, vec![20.0], "the page's own text keeps its size");
}