docspec-docx-reader 1.21.8

DOCX to DocSpec event stream reader
Documentation
//! Cross-crate integration test: an embedded `DocxReader` image asset must
//! flow through `BlockNoteWriter` as a base64 data URI.
//!
//! Reader-only assertions for embedded images (event shape, asset bytes,
//! content type) are exercised by the pandoc snapshot corpus through the
//! `AssetDescriptor` recorded in `tests/snapshots/docx/pandoc/image.docx.snap`;
//! this file covers only the multi-crate pipeline contract that snapshots
//! cannot express.
#![allow(
    clippy::expect_used,
    clippy::indexing_slicing,
    clippy::panic,
    clippy::redundant_test_prefix,
    clippy::separated_literal_suffix,
    clippy::tests_outside_test_module,
    clippy::unseparated_literal_suffix,
    clippy::unwrap_used
)]

mod fixture;

use std::io::Cursor;

use docspec_blocknote_writer::BlockNoteWriter;
use docspec_core::StackTrackingSink;
use docspec_core::{EventSink as _, EventSource as _};
use docspec_docx_reader::DocxReader;
use zip::CompressionMethod;

const PNG_SIGNATURE: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];

fn root_rels() -> &'static str {
    r#"<?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>"#
}

fn doc_rels_with_image() -> &'static str {
    r#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
</Relationships>"#
}

fn content_types_xml() -> &'static str {
    r#"<?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="png" ContentType="image/png"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>"#
}

fn document_with_embedded_image() -> &'static str {
    r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
    xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
    xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"
    xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing">
  <w:body><w:p><w:r><w:drawing>
    <wp:inline>
      <wp:docPr descr="alt text"/>
      <a:graphic><a:graphicData>
        <pic:pic><pic:blipFill><a:blip r:embed="rId4"/></pic:blipFill></pic:pic>
      </a:graphicData></a:graphic>
    </wp:inline>
  </w:drawing></w:r></w:p></w:body>
</w:document>"#
}

fn synth_image_docx() -> Vec<u8> {
    fixture::synth_docx_with_entries(&[
        (
            "_rels/.rels",
            CompressionMethod::Deflated,
            root_rels().as_bytes(),
        ),
        (
            "[Content_Types].xml",
            CompressionMethod::Deflated,
            content_types_xml().as_bytes(),
        ),
        (
            "word/_rels/document.xml.rels",
            CompressionMethod::Deflated,
            doc_rels_with_image().as_bytes(),
        ),
        (
            "word/document.xml",
            CompressionMethod::Deflated,
            document_with_embedded_image().as_bytes(),
        ),
        (
            "word/media/image1.png",
            CompressionMethod::Stored,
            &PNG_SIGNATURE,
        ),
    ])
}

#[test]
fn blocknote_writer_produces_data_uri_from_handle() {
    let bytes = synth_image_docx();
    let mut reader = DocxReader::from_reader(Cursor::new(bytes)).unwrap();

    let mut out = Vec::<u8>::new();
    let mut writer = StackTrackingSink::new(BlockNoteWriter::new(&mut out));

    loop {
        match reader.next_event() {
            Ok(Some(event)) => writer.handle_event(event).unwrap(),
            Ok(None) => break,
            Err(e) => panic!("reader error: {e:?}"),
        }
    }
    writer.finish().unwrap();

    let json: serde_json::Value =
        serde_json::from_slice(&out).expect("BlockNoteWriter output must be valid JSON");
    let blocks = json.as_array().expect("expected top-level JSON array");

    let image_block = blocks
        .iter()
        .find(|b| b["type"] == "image")
        .expect("expected at least one image block in JSON output");

    let url = image_block["props"]["url"]
        .as_str()
        .expect("expected url field to be a string");

    assert_eq!(url, "data:image/png;base64,iVBORw0KGgo=");
}