diff --git a/crates/docx/src/read.rs b/crates/docx/src/read.rs
index 3e6ea5e61..d50625285 100644
--- a/crates/docx/src/read.rs
+++ b/crates/docx/src/read.rs
@@ -197,7 +197,24 @@ fn read_opened_in(pkg: &Package, chunks: ChunkState) -> Result<(Document, usize,
r.doc.missing_pictures.main = main.clone();
r.pc.major_font = r.doc.settings.major_font.clone();
r.pc.minor_font = r.doc.settings.minor_font.clone();
- let part = |t: &str| rels.by_type(t).filter(|x| !x.external).map(|x| x.target.clone());
+ let part = |t: &str| {
+ let related = rels.by_type(t).filter(|x| !x.external).map(|x| x.target.clone()).filter(|p| pkg.get(p).is_some());
+ related.or_else(|| {
+ // Older DesignCraft imports read these two conventional parts directly. Keep opening
+ // those files when a producer omitted the document relationship; the bounded package
+ // reader still applies the same entry and XML limits. An explicit usable relationship
+ // always wins, including when it points outside the conventional `word/` paths.
+ if !main.eq_ignore_ascii_case("word/document.xml") {
+ return None;
+ }
+ let fallback = match t {
+ rt::STYLES => "word/styles.xml",
+ rt::NUMBERING => "word/numbering.xml",
+ _ => return None,
+ };
+ pkg.get(fallback).map(|_| fallback.to_string())
+ })
+ };
// The font table and the fonts embedded through it ride along as opaque parts.
if let Some(p) = part(rt::FONT_TABLE) {
diff --git a/crates/docx/tests/it/undefined_style.rs b/crates/docx/tests/it/undefined_style.rs
index c50394a2a..e1a2611cb 100644
--- a/crates/docx/tests/it/undefined_style.rs
+++ b/crates/docx/tests/it/undefined_style.rs
@@ -69,3 +69,37 @@ fn an_undefined_style_is_written_back_as_it_was_named() {
assert_eq!(footer_para(&again).props.style.as_deref(), Some("Footer"));
assert_eq!(again.styles.resolve_para(&footer_para(&again).props).space_after, 2.0);
}
+
+#[test]
+fn conventional_styles_and_numbering_parts_without_relationships_are_read() {
+ // The old DesignCraft reader opened these conventional paths directly. Keep that tolerated
+ // input behavior while preferring OPC relationships whenever they resolve successfully.
+ let doc = format!(
+ r#"text"#
+ );
+ let styles = format!(r#""#);
+ let numbering = format!(
+ r#""#
+ );
+ for main_path in ["word/document.xml", "WORD/DOCUMENT.XML"] {
+ let root = format!(
+ r#""#
+ );
+ let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
+ for (name, body) in [
+ ("_rels/.rels", root.as_str()),
+ (main_path, doc.as_str()),
+ ("word/styles.xml", styles.as_str()),
+ ("word/numbering.xml", numbering.as_str()),
+ ] {
+ zw.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
+ zw.write_all(body.as_bytes()).unwrap();
+ }
+ let d = craft_docx::read(&zw.finish().unwrap().into_inner()).unwrap();
+ assert!(d.styles.styles.iter().any(|style| style.id == "Body"));
+ let Some(Block::Para(p)) = d.body.first().map(|b| &**b) else { panic!("a body paragraph") };
+ assert_eq!(p.props.style.as_deref(), Some("Body"));
+ assert_eq!(p.props.numbering.map(|n| (n.num, n.level)), Some((7, 0)));
+ assert!(d.numbering.level(7, 0).is_some());
+ }
+}