use crate::document::{
AppProperties, CoreProperties, serialize_app_properties, serialize_core_properties,
};
use crate::error::Result;
use crate::generated_serializers::ToXml;
use crate::types;
use ooxml_opc::{PackageWriter, Relationship, Relationships, content_type, rel_type};
use ooxml_xml::{PositionedNode, RawXmlElement, RawXmlNode};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufWriter, Seek, Write};
use std::path::Path;
pub const NS_W: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
pub const NS_R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
pub const NS_WP: &str = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
pub const NS_A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
pub const NS_PIC: &str = "http://schemas.openxmlformats.org/drawingml/2006/picture";
pub const NS_WPS: &str = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
pub const NS_MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
const NS_DECLS: &[(&str, &str)] = &[
("xmlns:w", NS_W),
("xmlns:r", NS_R),
("xmlns:wp", NS_WP),
("xmlns:a", NS_A),
("xmlns:pic", NS_PIC),
];
#[derive(Clone)]
pub struct PendingImage {
pub data: Vec<u8>,
pub content_type: String,
pub rel_id: String,
pub filename: String,
}
#[derive(Clone)]
pub struct PendingHyperlink {
pub rel_id: String,
pub url: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListType {
Bullet,
Decimal,
LowerLetter,
UpperLetter,
LowerRoman,
UpperRoman,
}
#[derive(Clone)]
pub struct PendingNumbering {
pub abstract_num_id: u32,
pub num_id: u32,
pub list_type: Option<ListType>,
pub custom_levels: Option<Vec<NumberingLevel>>,
}
#[derive(Debug, Clone)]
pub struct NumberingLevel {
pub ilvl: u32,
pub format: ListType,
pub start: u32,
pub text: String,
pub indent_left: Option<u32>,
pub hanging: Option<u32>,
}
impl NumberingLevel {
pub fn bullet(ilvl: u32) -> Self {
Self {
ilvl,
format: ListType::Bullet,
start: 1,
text: "\u{2022}".to_string(),
indent_left: Some(720 * (ilvl + 1)),
hanging: Some(360),
}
}
pub fn decimal(ilvl: u32) -> Self {
Self {
ilvl,
format: ListType::Decimal,
start: 1,
text: format!("%{}.", ilvl + 1),
indent_left: Some(720 * (ilvl + 1)),
hanging: Some(360),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HeaderFooterType {
#[default]
Default,
First,
Even,
}
impl HeaderFooterType {
pub fn parse(s: &str) -> Self {
match s {
"first" => Self::First,
"even" => Self::Even,
_ => Self::Default,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Default => "default",
Self::First => "first",
Self::Even => "even",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WrapType {
#[default]
None,
Square,
Tight,
Through,
TopAndBottom,
}
#[derive(Clone)]
pub struct PendingHeader {
pub body: types::HeaderFooter,
pub rel_id: String,
pub header_type: HeaderFooterType,
pub filename: String,
}
#[derive(Clone)]
pub struct PendingFooter {
pub body: types::HeaderFooter,
pub rel_id: String,
pub footer_type: HeaderFooterType,
pub filename: String,
}
#[derive(Clone)]
pub struct PendingFootnote {
pub id: i32,
pub body: types::FootnoteEndnote,
}
#[derive(Clone)]
pub struct PendingEndnote {
pub id: i32,
pub body: types::FootnoteEndnote,
}
#[cfg(feature = "wml-settings")]
#[derive(Debug, Clone, Default)]
pub struct DocumentSettingsOptions {
pub default_tab_stop: Option<u32>,
pub even_and_odd_headers: bool,
pub track_changes: bool,
pub rsid_root: Option<String>,
pub compat_mode: bool,
}
#[cfg(feature = "wml-charts")]
#[derive(Clone)]
pub struct PendingChart {
pub data: Vec<u8>,
pub rel_id: String,
pub filename: String,
}
#[derive(Clone)]
pub struct PendingComment {
pub id: i32,
pub author: Option<String>,
pub date: Option<String>,
pub initials: Option<String>,
pub body: types::Comment,
}
pub struct HeaderBuilder<'a> {
builder: &'a mut DocumentBuilder,
rel_id: String,
}
impl<'a> HeaderBuilder<'a> {
pub fn body_mut(&mut self) -> &mut types::HeaderFooter {
&mut self
.builder
.headers
.get_mut(&self.rel_id)
.expect("header should exist")
.body
}
pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
self.body_mut().add_paragraph().add_run().set_text(text);
self
}
pub fn rel_id(&self) -> &str {
&self.rel_id
}
}
pub struct FooterBuilder<'a> {
builder: &'a mut DocumentBuilder,
rel_id: String,
}
impl<'a> FooterBuilder<'a> {
pub fn body_mut(&mut self) -> &mut types::HeaderFooter {
&mut self
.builder
.footers
.get_mut(&self.rel_id)
.expect("footer should exist")
.body
}
pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
self.body_mut().add_paragraph().add_run().set_text(text);
self
}
pub fn rel_id(&self) -> &str {
&self.rel_id
}
}
pub struct FootnoteBuilder<'a> {
builder: &'a mut DocumentBuilder,
id: i32,
}
impl<'a> FootnoteBuilder<'a> {
pub fn body_mut(&mut self) -> &mut types::FootnoteEndnote {
&mut self
.builder
.footnotes
.get_mut(&self.id)
.expect("footnote should exist")
.body
}
pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
self.body_mut().add_paragraph().add_run().set_text(text);
self
}
pub fn id(&self) -> u32 {
self.id as u32
}
}
pub struct EndnoteBuilder<'a> {
builder: &'a mut DocumentBuilder,
id: i32,
}
impl<'a> EndnoteBuilder<'a> {
pub fn body_mut(&mut self) -> &mut types::FootnoteEndnote {
&mut self
.builder
.endnotes
.get_mut(&self.id)
.expect("endnote should exist")
.body
}
pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
self.body_mut().add_paragraph().add_run().set_text(text);
self
}
pub fn id(&self) -> u32 {
self.id as u32
}
}
pub struct CommentBuilder<'a> {
builder: &'a mut DocumentBuilder,
id: i32,
}
impl<'a> CommentBuilder<'a> {
pub fn body_mut(&mut self) -> &mut types::Comment {
&mut self
.builder
.comments
.get_mut(&self.id)
.expect("comment should exist")
.body
}
pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
self.body_mut().add_paragraph().add_run().set_text(text);
self
}
pub fn set_author(&mut self, author: &str) -> &mut Self {
self.builder
.comments
.get_mut(&self.id)
.expect("comment should exist")
.author = Some(author.to_string());
self
}
pub fn set_date(&mut self, date: &str) -> &mut Self {
self.builder
.comments
.get_mut(&self.id)
.expect("comment should exist")
.date = Some(date.to_string());
self
}
pub fn set_initials(&mut self, initials: &str) -> &mut Self {
self.builder
.comments
.get_mut(&self.id)
.expect("comment should exist")
.initials = Some(initials.to_string());
self
}
pub fn id(&self) -> u32 {
self.id as u32
}
}
#[derive(Debug, Clone)]
pub struct TextBox {
pub text: String,
pub width_emu: i64,
pub height_emu: i64,
}
impl TextBox {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
width_emu: 914400, height_emu: 457200, }
}
pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
self.width_emu = emu;
self
}
pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
self.height_emu = emu;
self
}
pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
self.width_emu = (inches * 914400.0) as i64;
self
}
pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
self.height_emu = (inches * 914400.0) as i64;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Drawing {
images: Vec<InlineImage>,
anchored_images: Vec<AnchoredImage>,
text_boxes: Vec<TextBox>,
}
impl Drawing {
pub fn new() -> Self {
Self::default()
}
pub fn images(&self) -> &[InlineImage] {
&self.images
}
pub fn images_mut(&mut self) -> &mut Vec<InlineImage> {
&mut self.images
}
pub fn add_image(&mut self, rel_id: impl Into<String>) -> &mut InlineImage {
self.images.push(InlineImage::new(rel_id));
self.images.last_mut().unwrap()
}
pub fn anchored_images(&self) -> &[AnchoredImage] {
&self.anchored_images
}
pub fn anchored_images_mut(&mut self) -> &mut Vec<AnchoredImage> {
&mut self.anchored_images
}
pub fn add_anchored_image(&mut self, rel_id: impl Into<String>) -> &mut AnchoredImage {
self.anchored_images.push(AnchoredImage::new(rel_id));
self.anchored_images.last_mut().unwrap()
}
pub fn add_text_box(&mut self, text: impl Into<String>) -> &mut TextBox {
self.text_boxes.push(TextBox::new(text));
self.text_boxes.last_mut().unwrap()
}
pub fn text_boxes(&self) -> &[TextBox] {
&self.text_boxes
}
pub fn build(self, doc_id: &mut usize) -> types::CTDrawing {
let mut children = Vec::new();
let mut child_idx = 0usize;
for image in &self.images {
let elem = build_inline_image_element(image, *doc_id);
children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
child_idx += 1;
*doc_id += 1;
}
for image in &self.anchored_images {
let elem = build_anchored_image_element(image, *doc_id);
children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
child_idx += 1;
*doc_id += 1;
}
for text_box in &self.text_boxes {
let elem = build_text_box_element(text_box, *doc_id);
children.push(PositionedNode::new(child_idx, RawXmlNode::Element(elem)));
child_idx += 1;
*doc_id += 1;
}
types::CTDrawing {
#[cfg(feature = "extra-children")]
extra_children: children,
}
}
}
#[derive(Debug, Clone)]
pub struct InlineImage {
rel_id: String,
width_emu: Option<i64>,
height_emu: Option<i64>,
description: Option<String>,
}
impl InlineImage {
pub fn new(rel_id: impl Into<String>) -> Self {
Self {
rel_id: rel_id.into(),
width_emu: None,
height_emu: None,
description: None,
}
}
pub fn rel_id(&self) -> &str {
&self.rel_id
}
pub fn width_emu(&self) -> Option<i64> {
self.width_emu
}
pub fn height_emu(&self) -> Option<i64> {
self.height_emu
}
pub fn width_inches(&self) -> Option<f64> {
self.width_emu.map(|e| e as f64 / 914400.0)
}
pub fn height_inches(&self) -> Option<f64> {
self.height_emu.map(|e| e as f64 / 914400.0)
}
pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
self.width_emu = Some(emu);
self
}
pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
self.height_emu = Some(emu);
self
}
pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
self.width_emu = Some((inches * 914400.0) as i64);
self
}
pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
self.height_emu = Some((inches * 914400.0) as i64);
self
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn set_description(&mut self, desc: impl Into<String>) -> &mut Self {
self.description = Some(desc.into());
self
}
}
#[derive(Debug, Clone)]
pub struct AnchoredImage {
rel_id: String,
width_emu: Option<i64>,
height_emu: Option<i64>,
description: Option<String>,
behind_doc: bool,
pos_x: i64,
pos_y: i64,
wrap_type: WrapType,
}
impl AnchoredImage {
pub fn new(rel_id: impl Into<String>) -> Self {
Self {
rel_id: rel_id.into(),
width_emu: None,
height_emu: None,
description: None,
behind_doc: false,
pos_x: 0,
pos_y: 0,
wrap_type: WrapType::None,
}
}
pub fn rel_id(&self) -> &str {
&self.rel_id
}
pub fn width_emu(&self) -> Option<i64> {
self.width_emu
}
pub fn height_emu(&self) -> Option<i64> {
self.height_emu
}
pub fn width_inches(&self) -> Option<f64> {
self.width_emu.map(|e| e as f64 / 914400.0)
}
pub fn height_inches(&self) -> Option<f64> {
self.height_emu.map(|e| e as f64 / 914400.0)
}
pub fn set_width_emu(&mut self, emu: i64) -> &mut Self {
self.width_emu = Some(emu);
self
}
pub fn set_height_emu(&mut self, emu: i64) -> &mut Self {
self.height_emu = Some(emu);
self
}
pub fn set_width_inches(&mut self, inches: f64) -> &mut Self {
self.width_emu = Some((inches * 914400.0) as i64);
self
}
pub fn set_height_inches(&mut self, inches: f64) -> &mut Self {
self.height_emu = Some((inches * 914400.0) as i64);
self
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn set_description(&mut self, desc: impl Into<String>) -> &mut Self {
self.description = Some(desc.into());
self
}
pub fn is_behind_doc(&self) -> bool {
self.behind_doc
}
pub fn set_behind_doc(&mut self, behind: bool) -> &mut Self {
self.behind_doc = behind;
self
}
pub fn pos_x(&self) -> i64 {
self.pos_x
}
pub fn pos_y(&self) -> i64 {
self.pos_y
}
pub fn set_pos_x(&mut self, emu: i64) -> &mut Self {
self.pos_x = emu;
self
}
pub fn set_pos_y(&mut self, emu: i64) -> &mut Self {
self.pos_y = emu;
self
}
pub fn wrap_type(&self) -> WrapType {
self.wrap_type
}
pub fn set_wrap_type(&mut self, wrap: WrapType) -> &mut Self {
self.wrap_type = wrap;
self
}
}
pub struct DocumentBuilder {
document: types::Document,
images: HashMap<String, PendingImage>,
hyperlinks: HashMap<String, PendingHyperlink>,
numberings: HashMap<u32, PendingNumbering>,
styles: Option<types::Styles>,
headers: HashMap<String, PendingHeader>,
footers: HashMap<String, PendingFooter>,
footnotes: HashMap<i32, PendingFootnote>,
endnotes: HashMap<i32, PendingEndnote>,
comments: HashMap<i32, PendingComment>,
#[cfg(feature = "wml-settings")]
settings: Option<DocumentSettingsOptions>,
#[cfg(feature = "wml-charts")]
charts: HashMap<String, PendingChart>,
#[cfg(feature = "wml-charts")]
next_chart_id: u32,
core_properties: Option<CoreProperties>,
app_properties: Option<AppProperties>,
next_rel_id: u32,
next_num_id: u32,
next_header_id: u32,
next_footer_id: u32,
next_footnote_id: i32,
next_endnote_id: i32,
next_comment_id: i32,
next_drawing_id: usize,
}
impl Default for DocumentBuilder {
fn default() -> Self {
Self::new()
}
}
impl DocumentBuilder {
pub fn new() -> Self {
let document = types::Document {
#[cfg(feature = "wml-styling")]
background: None,
body: Some(Box::new(types::Body::default())),
conformance: None,
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
Self {
document,
images: HashMap::new(),
hyperlinks: HashMap::new(),
numberings: HashMap::new(),
styles: None,
headers: HashMap::new(),
footers: HashMap::new(),
footnotes: HashMap::new(),
endnotes: HashMap::new(),
comments: HashMap::new(),
#[cfg(feature = "wml-settings")]
settings: None,
#[cfg(feature = "wml-charts")]
charts: HashMap::new(),
#[cfg(feature = "wml-charts")]
next_chart_id: 1,
core_properties: None,
app_properties: None,
next_rel_id: 1,
next_num_id: 1,
next_header_id: 1,
next_footer_id: 1,
next_footnote_id: 1,
next_endnote_id: 1,
next_comment_id: 0,
next_drawing_id: 1,
}
}
pub fn add_image(&mut self, data: Vec<u8>, content_type: &str) -> String {
let id = self.next_rel_id;
self.next_rel_id += 1;
let rel_id = format!("rId{}", id);
let ext = extension_from_content_type(content_type);
let filename = format!("image{}.{}", id, ext);
self.images.insert(
rel_id.clone(),
PendingImage {
data,
content_type: content_type.to_string(),
rel_id: rel_id.clone(),
filename,
},
);
rel_id
}
pub fn add_hyperlink(&mut self, url: &str) -> String {
let id = self.next_rel_id;
self.next_rel_id += 1;
let rel_id = format!("rId{}", id);
self.hyperlinks.insert(
rel_id.clone(),
PendingHyperlink {
rel_id: rel_id.clone(),
url: url.to_string(),
},
);
rel_id
}
pub fn add_list(&mut self, list_type: ListType) -> u32 {
let num_id = self.next_num_id;
self.next_num_id += 1;
self.numberings.insert(
num_id,
PendingNumbering {
abstract_num_id: num_id, num_id,
list_type: Some(list_type),
custom_levels: None,
},
);
num_id
}
pub fn add_custom_list(&mut self, levels: Vec<NumberingLevel>) -> u32 {
let num_id = self.next_num_id;
self.next_num_id += 1;
self.numberings.insert(
num_id,
PendingNumbering {
abstract_num_id: num_id,
num_id,
list_type: None,
custom_levels: Some(levels),
},
);
num_id
}
pub fn set_styles(&mut self, styles: types::Styles) -> &mut Self {
self.styles = Some(styles);
self
}
pub fn add_style(&mut self, style: types::Style) -> &mut Self {
self.styles
.get_or_insert_with(types::Styles::default)
.style
.push(style);
self
}
pub fn add_header(&mut self, header_type: HeaderFooterType) -> HeaderBuilder<'_> {
let id = self.next_rel_id;
self.next_rel_id += 1;
let header_num = self.next_header_id;
self.next_header_id += 1;
let rel_id = format!("rId{}", id);
let filename = format!("header{}.xml", header_num);
self.headers.insert(
rel_id.clone(),
PendingHeader {
body: types::HeaderFooter::default(),
rel_id: rel_id.clone(),
header_type,
filename,
},
);
HeaderBuilder {
builder: self,
rel_id,
}
}
pub fn add_footer(&mut self, footer_type: HeaderFooterType) -> FooterBuilder<'_> {
let id = self.next_rel_id;
self.next_rel_id += 1;
let footer_num = self.next_footer_id;
self.next_footer_id += 1;
let rel_id = format!("rId{}", id);
let filename = format!("footer{}.xml", footer_num);
self.footers.insert(
rel_id.clone(),
PendingFooter {
body: types::HeaderFooter::default(),
rel_id: rel_id.clone(),
footer_type,
filename,
},
);
FooterBuilder {
builder: self,
rel_id,
}
}
pub fn add_footnote(&mut self) -> FootnoteBuilder<'_> {
let id = self.next_footnote_id;
self.next_footnote_id += 1;
self.footnotes.insert(
id,
PendingFootnote {
id,
body: types::FootnoteEndnote {
#[cfg(feature = "wml-comments")]
r#type: None,
id: id as i64,
block_content: Vec::new(),
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
},
},
);
FootnoteBuilder { builder: self, id }
}
pub fn add_endnote(&mut self) -> EndnoteBuilder<'_> {
let id = self.next_endnote_id;
self.next_endnote_id += 1;
self.endnotes.insert(
id,
PendingEndnote {
id,
body: types::FootnoteEndnote {
#[cfg(feature = "wml-comments")]
r#type: None,
id: id as i64,
block_content: Vec::new(),
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
},
},
);
EndnoteBuilder { builder: self, id }
}
pub fn add_comment(&mut self) -> CommentBuilder<'_> {
let id = self.next_comment_id;
self.next_comment_id += 1;
self.comments.insert(
id,
PendingComment {
id,
author: None,
date: None,
initials: None,
body: types::Comment {
id: 0, author: String::new(), #[cfg(feature = "wml-comments")]
date: None,
block_content: Vec::new(),
#[cfg(feature = "wml-comments")]
initials: None,
#[cfg(feature = "extra-attrs")]
extra_attrs: Default::default(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
},
},
);
CommentBuilder { builder: self, id }
}
pub fn set_core_properties(&mut self, props: CoreProperties) -> &mut Self {
self.core_properties = Some(props);
self
}
pub fn set_app_properties(&mut self, props: AppProperties) -> &mut Self {
self.app_properties = Some(props);
self
}
#[cfg(feature = "wml-settings")]
pub fn set_settings(&mut self, opts: DocumentSettingsOptions) -> &mut Self {
self.settings = Some(opts);
self
}
#[cfg(feature = "wml-charts")]
pub fn embed_chart(&mut self, chart_xml: &[u8]) -> crate::error::Result<String> {
let id = self.next_rel_id;
self.next_rel_id += 1;
let chart_num = self.next_chart_id;
self.next_chart_id += 1;
let rel_id = format!("rId{}", id);
let filename = format!("chart{}.xml", chart_num);
self.charts.insert(
rel_id.clone(),
PendingChart {
data: chart_xml.to_vec(),
rel_id: rel_id.clone(),
filename,
},
);
Ok(rel_id)
}
pub fn body_mut(&mut self) -> &mut types::Body {
self.document
.body
.as_deref_mut()
.expect("document body should exist")
}
pub fn add_paragraph(&mut self, text: &str) -> &mut Self {
let para = self.body_mut().add_paragraph();
para.add_run().set_text(text);
self
}
pub fn build_drawing(&mut self, drawing: Drawing) -> types::CTDrawing {
drawing.build(&mut self.next_drawing_id)
}
pub fn save<P: AsRef<Path>>(self, path: P) -> Result<()> {
let file = File::create(path)?;
let writer = BufWriter::new(file);
self.write(writer)
}
pub fn write<W: Write + Seek>(mut self, writer: W) -> Result<()> {
let mut pkg = PackageWriter::new(writer);
pkg.add_default_content_type("rels", content_type::RELATIONSHIPS);
pkg.add_default_content_type("xml", content_type::XML);
pkg.add_default_content_type("png", "image/png");
pkg.add_default_content_type("jpg", "image/jpeg");
pkg.add_default_content_type("jpeg", "image/jpeg");
pkg.add_default_content_type("gif", "image/gif");
let mut doc_rels = Relationships::new();
if !self.headers.is_empty() || !self.footers.is_empty() {
#[cfg(feature = "wml-layout")]
{
let body = self.document.body.as_deref_mut().expect("document body");
if body.sect_pr.is_none() {
body.sect_pr = Some(Box::new(types::SectionProperties::default()));
}
let sect_pr = body.sect_pr.as_deref_mut().unwrap();
for header in self.headers.values() {
let hdr_ref = types::HeaderFooterReference {
id: header.rel_id.clone(),
r#type: match header.header_type {
HeaderFooterType::Default => types::STHdrFtr::Default,
HeaderFooterType::First => types::STHdrFtr::First,
HeaderFooterType::Even => types::STHdrFtr::Even,
},
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
};
sect_pr
.header_footer_refs
.push(types::HeaderFooterRef::HeaderReference(Box::new(hdr_ref)));
}
for footer in self.footers.values() {
let ftr_ref = types::HeaderFooterReference {
id: footer.rel_id.clone(),
r#type: match footer.footer_type {
HeaderFooterType::Default => types::STHdrFtr::Default,
HeaderFooterType::First => types::STHdrFtr::First,
HeaderFooterType::Even => types::STHdrFtr::Even,
},
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
};
sect_pr
.header_footer_refs
.push(types::HeaderFooterRef::FooterReference(Box::new(ftr_ref)));
}
}
}
#[cfg(feature = "extra-attrs")]
{
for &(key, value) in NS_DECLS {
self.document
.extra_attrs
.insert(key.to_string(), value.to_string());
}
}
let doc_xml = serialize_to_xml_bytes(&self.document, "w:document")?;
pkg.add_part(
"word/document.xml",
content_type::WORDPROCESSING_DOCUMENT,
&doc_xml,
)?;
let mut pkg_rels = Relationships::new();
pkg_rels.add(Relationship::new(
"rId1",
rel_type::OFFICE_DOCUMENT,
"word/document.xml",
));
if let Some(ref core_props) = self.core_properties {
let core_xml = serialize_core_properties(core_props)?;
pkg.add_part(
"docProps/core.xml",
content_type::CORE_PROPERTIES,
&core_xml,
)?;
pkg_rels.add(Relationship::new(
"rId2",
rel_type::CORE_PROPERTIES,
"docProps/core.xml",
));
}
if let Some(ref app_props) = self.app_properties {
let app_xml = serialize_app_properties(app_props)?;
pkg.add_part(
"docProps/app.xml",
content_type::EXTENDED_PROPERTIES,
&app_xml,
)?;
pkg_rels.add(Relationship::new(
"rId3",
rel_type::EXTENDED_PROPERTIES,
"docProps/app.xml",
));
}
pkg.add_part(
"_rels/.rels",
content_type::RELATIONSHIPS,
pkg_rels.serialize().as_bytes(),
)?;
for image in self.images.values() {
doc_rels.add(Relationship::new(
&image.rel_id,
rel_type::IMAGE,
format!("media/{}", image.filename),
));
let image_path = format!("word/media/{}", image.filename);
pkg.add_part(&image_path, &image.content_type, &image.data)?;
}
for hyperlink in self.hyperlinks.values() {
doc_rels.add(Relationship::external(
&hyperlink.rel_id,
rel_type::HYPERLINK,
&hyperlink.url,
));
}
for header in self.headers.values() {
let header_xml = serialize_with_namespaces(&header.body, "w:hdr")?;
let header_path = format!("word/{}", header.filename);
pkg.add_part(
&header_path,
content_type::WORDPROCESSING_HEADER,
&header_xml,
)?;
doc_rels.add(Relationship::new(
&header.rel_id,
rel_type::HEADER,
&header.filename,
));
}
for footer in self.footers.values() {
let footer_xml = serialize_with_namespaces(&footer.body, "w:ftr")?;
let footer_path = format!("word/{}", footer.filename);
pkg.add_part(
&footer_path,
content_type::WORDPROCESSING_FOOTER,
&footer_xml,
)?;
doc_rels.add(Relationship::new(
&footer.rel_id,
rel_type::FOOTER,
&footer.filename,
));
}
if !self.footnotes.is_empty() {
let fns = build_footnotes(&self.footnotes);
let footnotes_xml = serialize_with_namespaces(&fns, "w:footnotes")?;
pkg.add_part(
"word/footnotes.xml",
content_type::WORDPROCESSING_FOOTNOTES,
&footnotes_xml,
)?;
let footnotes_rel_id = format!("rId{}", self.next_rel_id);
self.next_rel_id += 1;
doc_rels.add(Relationship::new(
&footnotes_rel_id,
rel_type::FOOTNOTES,
"footnotes.xml",
));
}
if !self.endnotes.is_empty() {
let ens = build_endnotes(&self.endnotes);
let endnotes_xml = serialize_with_namespaces(&ens, "w:endnotes")?;
pkg.add_part(
"word/endnotes.xml",
content_type::WORDPROCESSING_ENDNOTES,
&endnotes_xml,
)?;
let endnotes_rel_id = format!("rId{}", self.next_rel_id);
self.next_rel_id += 1;
doc_rels.add(Relationship::new(
&endnotes_rel_id,
rel_type::ENDNOTES,
"endnotes.xml",
));
}
if !self.comments.is_empty() {
let comments = build_comments(&self.comments);
let comments_xml = serialize_with_namespaces(&comments, "w:comments")?;
pkg.add_part(
"word/comments.xml",
content_type::WORDPROCESSING_COMMENTS,
&comments_xml,
)?;
let comments_rel_id = format!("rId{}", self.next_rel_id);
self.next_rel_id += 1;
doc_rels.add(Relationship::new(
&comments_rel_id,
rel_type::COMMENTS,
"comments.xml",
));
}
if let Some(ref styles) = self.styles {
let styles_xml = serialize_with_namespaces(styles, "w:styles")?;
pkg.add_part(
"word/styles.xml",
content_type::WORDPROCESSING_STYLES,
&styles_xml,
)?;
let styles_rel_id = format!("rId{}", self.next_rel_id);
self.next_rel_id += 1;
doc_rels.add(Relationship::new(
&styles_rel_id,
rel_type::STYLES,
"styles.xml",
));
}
if !self.numberings.is_empty() {
let numbering = build_numbering(&self.numberings);
let num_xml = serialize_with_namespaces(&numbering, "w:numbering")?;
pkg.add_part(
"word/numbering.xml",
content_type::WORDPROCESSING_NUMBERING,
&num_xml,
)?;
let num_rel_id = format!("rId{}", self.next_rel_id);
self.next_rel_id += 1;
doc_rels.add(Relationship::new(
&num_rel_id,
rel_type::NUMBERING,
"numbering.xml",
));
}
#[cfg(feature = "wml-settings")]
if let Some(ref settings_opts) = self.settings {
let settings_xml = build_settings_xml(settings_opts);
pkg.add_part(
"word/settings.xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
settings_xml.as_bytes(),
)?;
let settings_rel_id = format!("rId{}", self.next_rel_id);
self.next_rel_id += 1;
doc_rels.add(Relationship::new(
&settings_rel_id,
rel_type::SETTINGS,
"settings.xml",
));
}
#[cfg(feature = "wml-charts")]
for chart in self.charts.values() {
let chart_path = format!("word/charts/{}", chart.filename);
pkg.add_part(
&chart_path,
"application/vnd.openxmlformats-officedocument.drawingml.chart+xml",
&chart.data,
)?;
doc_rels.add(Relationship::new(
&chart.rel_id,
rel_type::CHART,
format!("charts/{}", chart.filename),
));
}
pkg.add_part(
"word/_rels/document.xml.rels",
content_type::RELATIONSHIPS,
doc_rels.serialize().as_bytes(),
)?;
pkg.finish()?;
Ok(())
}
}
fn serialize_to_xml_bytes(value: &impl ToXml, tag: &str) -> Result<Vec<u8>> {
let inner = Vec::new();
let mut writer = quick_xml::Writer::new(inner);
value.write_element(tag, &mut writer)?;
let inner = writer.into_inner();
let mut buf = Vec::with_capacity(
b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".len() + inner.len(),
);
buf.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
buf.extend_from_slice(&inner);
Ok(buf)
}
fn serialize_with_namespaces(value: &impl ToXml, tag: &str) -> Result<Vec<u8>> {
use quick_xml::events::{BytesEnd, BytesStart, Event};
let inner = Vec::new();
let mut writer = quick_xml::Writer::new(inner);
let start = BytesStart::new(tag);
let start = value.write_attrs(start);
let mut start = start;
for &(key, val) in NS_DECLS {
start.push_attribute((key, val));
}
if value.is_empty_element() {
writer.write_event(Event::Empty(start))?;
} else {
writer.write_event(Event::Start(start))?;
value.write_children(&mut writer)?;
writer.write_event(Event::End(BytesEnd::new(tag)))?;
}
let inner = writer.into_inner();
let mut buf = Vec::with_capacity(
b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n".len() + inner.len(),
);
buf.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n");
buf.extend_from_slice(&inner);
Ok(buf)
}
fn build_separator_ftn_edn(id: i64, ftn_type: types::STFtnEdn) -> types::FootnoteEndnote {
let separator_content = match ftn_type {
types::STFtnEdn::Separator => types::RunContent::Separator(Box::new(types::CTEmpty)),
types::STFtnEdn::ContinuationSeparator => {
types::RunContent::ContinuationSeparator(Box::new(types::CTEmpty))
}
_ => unreachable!("only Separator and ContinuationSeparator expected"),
};
let run = types::Run {
#[cfg(feature = "wml-track-changes")]
rsid_r_pr: None,
#[cfg(feature = "wml-track-changes")]
rsid_del: None,
#[cfg(feature = "wml-track-changes")]
rsid_r: None,
#[cfg(feature = "wml-styling")]
r_pr: None,
run_content: vec![separator_content],
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
let para = types::Paragraph {
#[cfg(feature = "wml-track-changes")]
rsid_r_pr: None,
#[cfg(feature = "wml-track-changes")]
rsid_r: None,
#[cfg(feature = "wml-track-changes")]
rsid_del: None,
#[cfg(feature = "wml-track-changes")]
rsid_p: None,
#[cfg(feature = "wml-track-changes")]
rsid_r_default: None,
#[cfg(feature = "wml-styling")]
p_pr: None,
paragraph_content: vec![types::ParagraphContent::R(Box::new(run))],
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
types::FootnoteEndnote {
#[cfg(feature = "wml-comments")]
r#type: Some(ftn_type),
id,
block_content: vec![types::BlockContent::P(Box::new(para))],
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
}
}
fn build_footnotes(footnotes: &HashMap<i32, PendingFootnote>) -> types::Footnotes {
let mut fns = types::Footnotes {
footnote: Vec::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
fns.footnote
.push(build_separator_ftn_edn(-1, types::STFtnEdn::Separator));
fns.footnote.push(build_separator_ftn_edn(
0,
types::STFtnEdn::ContinuationSeparator,
));
let mut sorted: Vec<_> = footnotes.values().collect();
sorted.sort_by_key(|f| f.id);
for footnote in sorted {
fns.footnote.push(footnote.body.clone());
}
fns
}
fn build_endnotes(endnotes: &HashMap<i32, PendingEndnote>) -> types::Endnotes {
let mut ens = types::Endnotes {
endnote: Vec::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
ens.endnote
.push(build_separator_ftn_edn(-1, types::STFtnEdn::Separator));
ens.endnote.push(build_separator_ftn_edn(
0,
types::STFtnEdn::ContinuationSeparator,
));
let mut sorted: Vec<_> = endnotes.values().collect();
sorted.sort_by_key(|e| e.id);
for endnote in sorted {
ens.endnote.push(endnote.body.clone());
}
ens
}
fn build_comments(comments: &HashMap<i32, PendingComment>) -> types::Comments {
let mut result = types::Comments {
comment: Vec::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
let mut sorted: Vec<_> = comments.values().collect();
sorted.sort_by_key(|c| c.id);
for pc in sorted {
let mut comment = pc.body.clone();
comment.id = pc.id as i64;
if let Some(ref author) = pc.author {
comment.author = author.clone();
}
#[cfg(feature = "wml-comments")]
if let Some(ref date) = pc.date {
comment.date = Some(date.clone());
}
#[cfg(feature = "wml-comments")]
if let Some(ref initials) = pc.initials {
comment.initials = Some(initials.clone());
}
result.comment.push(comment);
}
result
}
fn list_type_to_num_fmt_and_text(list_type: ListType) -> (types::STNumberFormat, &'static str) {
match list_type {
ListType::Bullet => (types::STNumberFormat::Bullet, "\u{2022}"),
ListType::Decimal => (types::STNumberFormat::Decimal, "%1."),
ListType::LowerLetter => (types::STNumberFormat::LowerLetter, "%1."),
ListType::UpperLetter => (types::STNumberFormat::UpperLetter, "%1."),
ListType::LowerRoman => (types::STNumberFormat::LowerRoman, "%1."),
ListType::UpperRoman => (types::STNumberFormat::UpperRoman, "%1."),
}
}
#[cfg(feature = "wml-numbering")]
fn list_type_to_num_fmt(list_type: ListType) -> types::STNumberFormat {
match list_type {
ListType::Bullet => types::STNumberFormat::Bullet,
ListType::Decimal => types::STNumberFormat::Decimal,
ListType::LowerLetter => types::STNumberFormat::LowerLetter,
ListType::UpperLetter => types::STNumberFormat::UpperLetter,
ListType::LowerRoman => types::STNumberFormat::LowerRoman,
ListType::UpperRoman => types::STNumberFormat::UpperRoman,
}
}
fn build_level_from_spec(spec: &NumberingLevel) -> types::Level {
#[cfg(feature = "wml-numbering")]
let is_bullet = spec.format == ListType::Bullet;
#[cfg(feature = "wml-numbering")]
let indent_left = spec.indent_left.unwrap_or(720 * (spec.ilvl + 1));
#[cfg(feature = "wml-numbering")]
let hanging = spec.hanging.unwrap_or(360);
types::Level {
ilvl: spec.ilvl as i64,
#[cfg(feature = "wml-numbering")]
tplc: None,
#[cfg(feature = "wml-numbering")]
tentative: None,
#[cfg(feature = "wml-numbering")]
start: Some(Box::new(types::CTDecimalNumber {
value: spec.start as i64,
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
})),
#[cfg(feature = "wml-numbering")]
num_fmt: Some(Box::new(types::CTNumFmt {
value: list_type_to_num_fmt(spec.format),
format: None,
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
})),
#[cfg(feature = "wml-numbering")]
lvl_restart: None,
#[cfg(feature = "wml-numbering")]
paragraph_style: None,
#[cfg(feature = "wml-numbering")]
is_lgl: None,
#[cfg(feature = "wml-numbering")]
suff: None,
#[cfg(feature = "wml-numbering")]
lvl_text: Some(Box::new(types::CTLevelText {
value: Some(spec.text.clone()),
null: None,
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
})),
#[cfg(feature = "wml-numbering")]
lvl_pic_bullet_id: None,
#[cfg(feature = "wml-numbering")]
legacy: None,
#[cfg(feature = "wml-numbering")]
lvl_jc: Some(Box::new(types::CTJc {
value: types::STJc::Left,
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
})),
#[cfg(feature = "wml-numbering")]
p_pr: Some(Box::new(build_level_paragraph_properties(
indent_left,
hanging,
))),
#[cfg(feature = "wml-numbering")]
r_pr: if is_bullet {
Some(Box::new(build_bullet_run_properties()))
} else {
None
},
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
}
}
#[cfg(feature = "wml-numbering")]
fn build_level_paragraph_properties(indent_left: u32, hanging: u32) -> types::CTPPrGeneral {
let ind = types::CTInd {
#[cfg(feature = "wml-styling")]
left: Some(indent_left.to_string()),
#[cfg(feature = "wml-styling")]
hanging: Some(hanging.to_string()),
..Default::default()
};
let _ = indent_left;
let _ = hanging;
types::CTPPrGeneral {
indentation: Some(Box::new(ind)),
..Default::default()
}
}
fn build_numbering(numberings: &HashMap<u32, PendingNumbering>) -> types::Numbering {
let mut numbering = types::Numbering {
#[cfg(feature = "wml-numbering")]
num_pic_bullet: Vec::new(),
abstract_num: Vec::new(),
num: Vec::new(),
#[cfg(feature = "wml-numbering")]
num_id_mac_at_cleanup: None,
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
let mut sorted: Vec<_> = numberings.values().collect();
sorted.sort_by_key(|n| n.num_id);
for pn in &sorted {
let levels: Vec<types::Level> = if let Some(ref custom_levels) = pn.custom_levels {
custom_levels.iter().map(build_level_from_spec).collect()
} else if let Some(list_type) = pn.list_type {
let (_num_fmt, _lvl_text) = list_type_to_num_fmt_and_text(list_type);
let spec = NumberingLevel {
ilvl: 0,
format: list_type,
start: 1,
text: _lvl_text.to_string(),
indent_left: Some(720),
hanging: Some(360),
};
vec![build_level_from_spec(&spec)]
} else {
Vec::new()
};
let abs = types::AbstractNumbering {
abstract_num_id: pn.abstract_num_id as i64,
#[cfg(feature = "wml-numbering")]
nsid: None,
#[cfg(feature = "wml-numbering")]
multi_level_type: None,
#[cfg(feature = "wml-numbering")]
tmpl: None,
#[cfg(feature = "wml-numbering")]
name: None,
#[cfg(feature = "wml-numbering")]
style_link: None,
#[cfg(feature = "wml-numbering")]
num_style_link: None,
lvl: levels,
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
numbering.abstract_num.push(abs);
let inst = types::NumberingInstance {
num_id: pn.num_id as i64,
abstract_num_id: Box::new(types::CTDecimalNumber {
value: pn.abstract_num_id as i64,
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
}),
#[cfg(feature = "wml-numbering")]
lvl_override: Vec::new(),
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
numbering.num.push(inst);
}
numbering
}
#[cfg(feature = "wml-styling")]
fn build_bullet_run_properties() -> types::RunProperties {
types::RunProperties {
fonts: Some(Box::new(types::Fonts {
ascii: Some("Symbol".to_string()),
h_ansi: Some("Symbol".to_string()),
hint: Some(types::STHint::Default),
..Default::default()
})),
..Default::default()
}
}
#[cfg(not(feature = "wml-styling"))]
#[allow(dead_code)]
fn build_bullet_run_properties() -> types::RunProperties {
types::RunProperties::default()
}
#[cfg(feature = "wml-settings")]
fn build_settings_xml(opts: &DocumentSettingsOptions) -> String {
let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
xml.push_str("\r\n");
xml.push_str(
r#"<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">"#,
);
if let Some(tab_stop) = opts.default_tab_stop {
xml.push_str(&format!(r#"<w:defaultTabStop w:val="{}"/>"#, tab_stop));
}
if opts.even_and_odd_headers {
xml.push_str("<w:evenAndOddHeaders/>");
}
if opts.track_changes {
xml.push_str("<w:trackChanges/>");
}
if let Some(ref rsid) = opts.rsid_root {
xml.push_str(&format!(r#"<w:rsidRoot w:val="{}"/>"#, rsid));
}
if opts.compat_mode {
xml.push_str(concat!(
"<w:compat>",
r#"<w:compatSetting w:name="compatibilityMode" "#,
r#"w:uri="http://schemas.microsoft.com/office/word" "#,
r#"w:val="15"/>"#,
"</w:compat>",
));
}
xml.push_str("</w:settings>");
xml
}
fn build_graphic_element(rel_id: &str, width: i64, height: i64, doc_id: usize) -> RawXmlElement {
let blip = RawXmlElement {
name: "a:blip".to_string(),
attributes: vec![("r:embed".to_string(), rel_id.to_string())],
children: vec![],
self_closing: true,
};
let fill_rect = RawXmlElement {
name: "a:fillRect".to_string(),
attributes: vec![],
children: vec![],
self_closing: true,
};
let stretch = RawXmlElement {
name: "a:stretch".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(fill_rect)],
self_closing: false,
};
let blip_fill = RawXmlElement {
name: "pic:blipFill".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(blip), RawXmlNode::Element(stretch)],
self_closing: false,
};
let cnv_pr = RawXmlElement {
name: "pic:cNvPr".to_string(),
attributes: vec![
("id".to_string(), doc_id.to_string()),
("name".to_string(), format!("Picture {}", doc_id)),
],
children: vec![],
self_closing: true,
};
let cnv_pic_pr = RawXmlElement {
name: "pic:cNvPicPr".to_string(),
attributes: vec![],
children: vec![],
self_closing: true,
};
let nv_pic_pr = RawXmlElement {
name: "pic:nvPicPr".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(cnv_pr), RawXmlNode::Element(cnv_pic_pr)],
self_closing: false,
};
let off = RawXmlElement {
name: "a:off".to_string(),
attributes: vec![
("x".to_string(), "0".to_string()),
("y".to_string(), "0".to_string()),
],
children: vec![],
self_closing: true,
};
let ext = RawXmlElement {
name: "a:ext".to_string(),
attributes: vec![
("cx".to_string(), width.to_string()),
("cy".to_string(), height.to_string()),
],
children: vec![],
self_closing: true,
};
let xfrm = RawXmlElement {
name: "a:xfrm".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(off), RawXmlNode::Element(ext)],
self_closing: false,
};
let av_lst = RawXmlElement {
name: "a:avLst".to_string(),
attributes: vec![],
children: vec![],
self_closing: true,
};
let prst_geom = RawXmlElement {
name: "a:prstGeom".to_string(),
attributes: vec![("prst".to_string(), "rect".to_string())],
children: vec![RawXmlNode::Element(av_lst)],
self_closing: false,
};
let sp_pr = RawXmlElement {
name: "pic:spPr".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(xfrm), RawXmlNode::Element(prst_geom)],
self_closing: false,
};
let pic = RawXmlElement {
name: "pic:pic".to_string(),
attributes: vec![],
children: vec![
RawXmlNode::Element(nv_pic_pr),
RawXmlNode::Element(blip_fill),
RawXmlNode::Element(sp_pr),
],
self_closing: false,
};
let graphic_data = RawXmlElement {
name: "a:graphicData".to_string(),
attributes: vec![(
"uri".to_string(),
"http://schemas.openxmlformats.org/drawingml/2006/picture".to_string(),
)],
children: vec![RawXmlNode::Element(pic)],
self_closing: false,
};
RawXmlElement {
name: "a:graphic".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(graphic_data)],
self_closing: false,
}
}
fn build_inline_image_element(image: &InlineImage, doc_id: usize) -> RawXmlElement {
let width_emu = image.width_emu.unwrap_or(914400);
let height_emu = image.height_emu.unwrap_or(914400);
let desc = image.description.as_deref().unwrap_or("Image");
let extent = RawXmlElement {
name: "wp:extent".to_string(),
attributes: vec![
("cx".to_string(), width_emu.to_string()),
("cy".to_string(), height_emu.to_string()),
],
children: vec![],
self_closing: true,
};
let doc_pr = RawXmlElement {
name: "wp:docPr".to_string(),
attributes: vec![
("id".to_string(), doc_id.to_string()),
("name".to_string(), format!("Picture {}", doc_id)),
("descr".to_string(), desc.to_string()),
],
children: vec![],
self_closing: true,
};
let graphic_frame_locks = RawXmlElement {
name: "a:graphicFrameLocks".to_string(),
attributes: vec![("noChangeAspect".to_string(), "1".to_string())],
children: vec![],
self_closing: true,
};
let cnv_graphic_frame_pr = RawXmlElement {
name: "wp:cNvGraphicFramePr".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(graphic_frame_locks)],
self_closing: false,
};
let graphic = build_graphic_element(&image.rel_id, width_emu, height_emu, doc_id);
RawXmlElement {
name: "wp:inline".to_string(),
attributes: vec![
("distT".to_string(), "0".to_string()),
("distB".to_string(), "0".to_string()),
("distL".to_string(), "0".to_string()),
("distR".to_string(), "0".to_string()),
],
children: vec![
RawXmlNode::Element(extent),
RawXmlNode::Element(doc_pr),
RawXmlNode::Element(cnv_graphic_frame_pr),
RawXmlNode::Element(graphic),
],
self_closing: false,
}
}
fn build_wrap_element(wrap_type: WrapType) -> RawXmlElement {
match wrap_type {
WrapType::None => RawXmlElement {
name: "wp:wrapNone".to_string(),
attributes: vec![],
children: vec![],
self_closing: true,
},
WrapType::Square => RawXmlElement {
name: "wp:wrapSquare".to_string(),
attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
children: vec![],
self_closing: true,
},
WrapType::Tight => {
let polygon = build_default_wrap_polygon();
RawXmlElement {
name: "wp:wrapTight".to_string(),
attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
children: vec![RawXmlNode::Element(polygon)],
self_closing: false,
}
}
WrapType::Through => {
let polygon = build_default_wrap_polygon();
RawXmlElement {
name: "wp:wrapThrough".to_string(),
attributes: vec![("wrapText".to_string(), "bothSides".to_string())],
children: vec![RawXmlNode::Element(polygon)],
self_closing: false,
}
}
WrapType::TopAndBottom => RawXmlElement {
name: "wp:wrapTopAndBottom".to_string(),
attributes: vec![],
children: vec![],
self_closing: true,
},
}
}
fn build_default_wrap_polygon() -> RawXmlElement {
let start = RawXmlElement {
name: "wp:start".to_string(),
attributes: vec![
("x".to_string(), "0".to_string()),
("y".to_string(), "0".to_string()),
],
children: vec![],
self_closing: true,
};
let line_to_1 = RawXmlElement {
name: "wp:lineTo".to_string(),
attributes: vec![
("x".to_string(), "0".to_string()),
("y".to_string(), "21600".to_string()),
],
children: vec![],
self_closing: true,
};
let line_to_2 = RawXmlElement {
name: "wp:lineTo".to_string(),
attributes: vec![
("x".to_string(), "21600".to_string()),
("y".to_string(), "21600".to_string()),
],
children: vec![],
self_closing: true,
};
let line_to_3 = RawXmlElement {
name: "wp:lineTo".to_string(),
attributes: vec![
("x".to_string(), "21600".to_string()),
("y".to_string(), "0".to_string()),
],
children: vec![],
self_closing: true,
};
let line_to_4 = RawXmlElement {
name: "wp:lineTo".to_string(),
attributes: vec![
("x".to_string(), "0".to_string()),
("y".to_string(), "0".to_string()),
],
children: vec![],
self_closing: true,
};
RawXmlElement {
name: "wp:wrapPolygon".to_string(),
attributes: vec![("edited".to_string(), "0".to_string())],
children: vec![
RawXmlNode::Element(start),
RawXmlNode::Element(line_to_1),
RawXmlNode::Element(line_to_2),
RawXmlNode::Element(line_to_3),
RawXmlNode::Element(line_to_4),
],
self_closing: false,
}
}
fn build_anchored_image_element(image: &AnchoredImage, doc_id: usize) -> RawXmlElement {
let width_emu = image.width_emu.unwrap_or(914400);
let height_emu = image.height_emu.unwrap_or(914400);
let desc = image.description.as_deref().unwrap_or("Image");
let behind_doc = if image.behind_doc { "1" } else { "0" };
let simple_pos = RawXmlElement {
name: "wp:simplePos".to_string(),
attributes: vec![
("x".to_string(), "0".to_string()),
("y".to_string(), "0".to_string()),
],
children: vec![],
self_closing: true,
};
let pos_offset_h = RawXmlElement {
name: "wp:posOffset".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Text(image.pos_x.to_string())],
self_closing: false,
};
let position_h = RawXmlElement {
name: "wp:positionH".to_string(),
attributes: vec![("relativeFrom".to_string(), "column".to_string())],
children: vec![RawXmlNode::Element(pos_offset_h)],
self_closing: false,
};
let pos_offset_v = RawXmlElement {
name: "wp:posOffset".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Text(image.pos_y.to_string())],
self_closing: false,
};
let position_v = RawXmlElement {
name: "wp:positionV".to_string(),
attributes: vec![("relativeFrom".to_string(), "paragraph".to_string())],
children: vec![RawXmlNode::Element(pos_offset_v)],
self_closing: false,
};
let extent = RawXmlElement {
name: "wp:extent".to_string(),
attributes: vec![
("cx".to_string(), width_emu.to_string()),
("cy".to_string(), height_emu.to_string()),
],
children: vec![],
self_closing: true,
};
let effect_extent = RawXmlElement {
name: "wp:effectExtent".to_string(),
attributes: vec![
("l".to_string(), "0".to_string()),
("t".to_string(), "0".to_string()),
("r".to_string(), "0".to_string()),
("b".to_string(), "0".to_string()),
],
children: vec![],
self_closing: true,
};
let wrap = build_wrap_element(image.wrap_type);
let doc_pr = RawXmlElement {
name: "wp:docPr".to_string(),
attributes: vec![
("id".to_string(), doc_id.to_string()),
("name".to_string(), format!("Picture {}", doc_id)),
("descr".to_string(), desc.to_string()),
],
children: vec![],
self_closing: true,
};
let graphic_frame_locks = RawXmlElement {
name: "a:graphicFrameLocks".to_string(),
attributes: vec![("noChangeAspect".to_string(), "1".to_string())],
children: vec![],
self_closing: true,
};
let cnv_graphic_frame_pr = RawXmlElement {
name: "wp:cNvGraphicFramePr".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(graphic_frame_locks)],
self_closing: false,
};
let graphic = build_graphic_element(&image.rel_id, width_emu, height_emu, doc_id);
RawXmlElement {
name: "wp:anchor".to_string(),
attributes: vec![
("distT".to_string(), "0".to_string()),
("distB".to_string(), "0".to_string()),
("distL".to_string(), "114300".to_string()),
("distR".to_string(), "114300".to_string()),
("simplePos".to_string(), "0".to_string()),
("relativeHeight".to_string(), "251658240".to_string()),
("behindDoc".to_string(), behind_doc.to_string()),
("locked".to_string(), "0".to_string()),
("layoutInCell".to_string(), "1".to_string()),
("allowOverlap".to_string(), "1".to_string()),
],
children: vec![
RawXmlNode::Element(simple_pos),
RawXmlNode::Element(position_h),
RawXmlNode::Element(position_v),
RawXmlNode::Element(extent),
RawXmlNode::Element(effect_extent),
RawXmlNode::Element(wrap),
RawXmlNode::Element(doc_pr),
RawXmlNode::Element(cnv_graphic_frame_pr),
RawXmlNode::Element(graphic),
],
self_closing: false,
}
}
fn build_text_box_element(text_box: &TextBox, doc_id: usize) -> RawXmlElement {
let w = text_box.width_emu;
let h = text_box.height_emu;
let off = RawXmlElement {
name: "a:off".to_string(),
attributes: vec![
("x".to_string(), "0".to_string()),
("y".to_string(), "0".to_string()),
],
children: vec![],
self_closing: true,
};
let ext = RawXmlElement {
name: "a:ext".to_string(),
attributes: vec![
("cx".to_string(), w.to_string()),
("cy".to_string(), h.to_string()),
],
children: vec![],
self_closing: true,
};
let xfrm = RawXmlElement {
name: "a:xfrm".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(off), RawXmlNode::Element(ext)],
self_closing: false,
};
let av_lst = RawXmlElement {
name: "a:avLst".to_string(),
attributes: vec![],
children: vec![],
self_closing: true,
};
let prst_geom = RawXmlElement {
name: "a:prstGeom".to_string(),
attributes: vec![("prst".to_string(), "rect".to_string())],
children: vec![RawXmlNode::Element(av_lst)],
self_closing: false,
};
let sp_pr = RawXmlElement {
name: "wps:spPr".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(xfrm), RawXmlNode::Element(prst_geom)],
self_closing: false,
};
let t_node = RawXmlElement {
name: "w:t".to_string(),
attributes: vec![("xml:space".to_string(), "preserve".to_string())],
children: vec![RawXmlNode::Text(text_box.text.clone())],
self_closing: false,
};
let r_node = RawXmlElement {
name: "w:r".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(t_node)],
self_closing: false,
};
let p_node = RawXmlElement {
name: "w:p".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(r_node)],
self_closing: false,
};
let txbx_content = RawXmlElement {
name: "w:txbxContent".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(p_node)],
self_closing: false,
};
let txbx = RawXmlElement {
name: "wps:txbx".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(txbx_content)],
self_closing: false,
};
let wsp = RawXmlElement {
name: "wps:wsp".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(sp_pr), RawXmlNode::Element(txbx)],
self_closing: false,
};
let graphic_data = RawXmlElement {
name: "a:graphicData".to_string(),
attributes: vec![("uri".to_string(), NS_WPS.to_string())],
children: vec![RawXmlNode::Element(wsp)],
self_closing: false,
};
let graphic = RawXmlElement {
name: "a:graphic".to_string(),
attributes: vec![],
children: vec![RawXmlNode::Element(graphic_data)],
self_closing: false,
};
let doc_pr = RawXmlElement {
name: "wp:docPr".to_string(),
attributes: vec![
("id".to_string(), doc_id.to_string()),
("name".to_string(), format!("Text Box {}", doc_id)),
],
children: vec![],
self_closing: true,
};
let extent = RawXmlElement {
name: "wp:extent".to_string(),
attributes: vec![
("cx".to_string(), w.to_string()),
("cy".to_string(), h.to_string()),
],
children: vec![],
self_closing: true,
};
RawXmlElement {
name: "wp:inline".to_string(),
attributes: vec![
("distT".to_string(), "0".to_string()),
("distB".to_string(), "0".to_string()),
("distL".to_string(), "0".to_string()),
("distR".to_string(), "0".to_string()),
],
children: vec![
RawXmlNode::Element(extent),
RawXmlNode::Element(doc_pr),
RawXmlNode::Element(graphic),
],
self_closing: false,
}
}
fn extension_from_content_type(content_type: &str) -> &'static str {
match content_type {
"image/png" => "png",
"image/jpeg" => "jpg",
"image/gif" => "gif",
"image/bmp" => "bmp",
"image/tiff" => "tiff",
"image/webp" => "webp",
"image/svg+xml" => "svg",
"image/x-emf" | "image/emf" => "emf",
"image/x-wmf" | "image/wmf" => "wmf",
_ => "bin",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_document_builder_simple() {
let mut builder = DocumentBuilder::new();
builder.add_paragraph("Hello, World!");
builder.add_paragraph("Second paragraph");
let body = builder.document.body.as_ref().unwrap();
assert_eq!(body.block_content.len(), 2);
}
#[test]
fn test_serialize_to_xml_bytes() {
let doc = types::Document {
background: None,
body: Some(Box::new(types::Body::default())),
conformance: None,
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
#[cfg(feature = "extra-children")]
extra_children: Vec::new(),
};
let bytes = serialize_to_xml_bytes(&doc, "w:document").unwrap();
let xml = String::from_utf8(bytes).unwrap();
assert!(xml.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"));
assert!(xml.contains("w:document"));
}
#[test]
fn test_list_type_mapping() {
let (fmt, text) = list_type_to_num_fmt_and_text(ListType::Bullet);
assert!(matches!(fmt, types::STNumberFormat::Bullet));
assert_eq!(text, "\u{2022}");
let (fmt, text) = list_type_to_num_fmt_and_text(ListType::Decimal);
assert!(matches!(fmt, types::STNumberFormat::Decimal));
assert_eq!(text, "%1.");
}
#[test]
fn test_extension_from_content_type() {
assert_eq!(extension_from_content_type("image/png"), "png");
assert_eq!(extension_from_content_type("image/jpeg"), "jpg");
assert_eq!(extension_from_content_type("image/gif"), "gif");
assert_eq!(extension_from_content_type("unknown/type"), "bin");
}
#[test]
fn test_drawing_build() {
let mut drawing = Drawing::new();
drawing
.add_image("rId1")
.set_width_inches(1.0)
.set_height_inches(1.0);
let mut doc_id = 1;
let ct_drawing = drawing.build(&mut doc_id);
assert_eq!(doc_id, 2);
#[cfg(feature = "extra-children")]
assert_eq!(ct_drawing.extra_children.len(), 1);
let _ = ct_drawing;
}
#[test]
fn test_text_box_build() {
let mut drawing = Drawing::new();
drawing
.add_text_box("Hello, text box!")
.set_width_inches(2.0)
.set_height_inches(1.0);
assert_eq!(drawing.text_boxes().len(), 1);
assert_eq!(drawing.text_boxes()[0].text, "Hello, text box!");
assert_eq!(drawing.text_boxes()[0].width_emu, (2.0 * 914400.0) as i64);
let mut doc_id = 1;
let ct_drawing = drawing.build(&mut doc_id);
assert_eq!(doc_id, 2);
#[cfg(feature = "extra-children")]
assert_eq!(ct_drawing.extra_children.len(), 1);
let _ = ct_drawing;
}
#[test]
#[cfg(all(feature = "extra-attrs", feature = "extra-children"))]
fn test_roundtrip_with_text_box() {
use crate::Document;
use std::io::Cursor;
let mut builder = DocumentBuilder::new();
{
let body = builder.body_mut();
let para = body.add_paragraph();
let run = para.add_run();
let mut drawing = Drawing::new();
drawing.add_text_box("My text box content");
let ct = drawing.build(&mut 1usize.clone());
run.add_drawing(ct);
}
let mut buf = Cursor::new(Vec::new());
builder.write(&mut buf).unwrap();
buf.set_position(0);
let doc = Document::from_reader(buf).unwrap();
let body = doc.body();
assert_eq!(body.block_content.len(), 1);
}
#[test]
fn test_add_custom_list() {
let mut builder = DocumentBuilder::new();
let num_id = builder.add_custom_list(vec![
NumberingLevel {
ilvl: 0,
format: ListType::Decimal,
start: 1,
text: "%1.".to_string(),
indent_left: Some(720),
hanging: Some(360),
},
NumberingLevel {
ilvl: 1,
format: ListType::LowerLetter,
start: 1,
text: "%2.".to_string(),
indent_left: Some(1440),
hanging: Some(360),
},
]);
assert_eq!(num_id, 1);
assert!(builder.numberings.contains_key(&1));
let pn = &builder.numberings[&1];
assert!(pn.custom_levels.is_some());
assert_eq!(pn.custom_levels.as_ref().unwrap().len(), 2);
}
#[test]
fn test_numbering_level_helpers() {
let bullet = NumberingLevel::bullet(0);
assert_eq!(bullet.ilvl, 0);
assert_eq!(bullet.format, ListType::Bullet);
assert_eq!(bullet.start, 1);
assert_eq!(bullet.indent_left, Some(720));
let decimal = NumberingLevel::decimal(2);
assert_eq!(decimal.ilvl, 2);
assert_eq!(decimal.format, ListType::Decimal);
assert_eq!(decimal.indent_left, Some(2160));
}
#[test]
#[cfg(all(
feature = "wml-numbering",
feature = "extra-attrs",
feature = "extra-children"
))]
fn test_roundtrip_custom_list() {
use crate::Document;
use crate::ext::BodyExt;
use std::io::Cursor;
let mut builder = DocumentBuilder::new();
let num_id = builder.add_custom_list(vec![NumberingLevel {
ilvl: 0,
format: ListType::Decimal,
start: 1,
text: "%1.".to_string(),
indent_left: Some(720),
hanging: Some(360),
}]);
{
let body = builder.body_mut();
let para = body.add_paragraph();
#[cfg(feature = "wml-styling")]
para.set_numbering(num_id, 0);
para.add_run().set_text("Item one");
}
let mut buf = Cursor::new(Vec::new());
builder.write(&mut buf).unwrap();
buf.set_position(0);
let doc = Document::from_reader(buf).unwrap();
assert_eq!(doc.body().paragraphs().len(), 1);
let _ = num_id;
}
#[test]
fn test_core_and_app_properties_roundtrip() {
use crate::Document;
use crate::document::{AppProperties, CoreProperties};
use std::io::Cursor;
let mut builder = DocumentBuilder::new();
builder.add_paragraph("Hello");
builder.set_core_properties(CoreProperties {
title: Some("Test Doc".to_string()),
creator: Some("Test Author".to_string()),
created: Some("2024-01-01T00:00:00Z".to_string()),
..Default::default()
});
builder.set_app_properties(AppProperties {
application: Some("ooxml-wml".to_string()),
pages: Some(1),
..Default::default()
});
let mut buffer = Cursor::new(Vec::new());
builder.write(&mut buffer).unwrap();
buffer.set_position(0);
let doc = Document::from_reader(buffer).unwrap();
let core = doc
.core_properties()
.expect("core properties should be present");
assert_eq!(core.title, Some("Test Doc".to_string()));
assert_eq!(core.creator, Some("Test Author".to_string()));
assert_eq!(core.created, Some("2024-01-01T00:00:00Z".to_string()));
let app = doc
.app_properties()
.expect("app properties should be present");
assert_eq!(app.application, Some("ooxml-wml".to_string()));
assert_eq!(app.pages, Some(1));
}
#[test]
fn test_roundtrip_create_and_read() {
use crate::Document;
use crate::ext::BodyExt;
use std::io::Cursor;
let mut builder = DocumentBuilder::new();
builder.add_paragraph("Test content");
let mut buffer = Cursor::new(Vec::new());
builder.write(&mut buffer).unwrap();
buffer.set_position(0);
let doc = Document::from_reader(buffer).unwrap();
assert_eq!(doc.body().paragraphs().len(), 1);
assert_eq!(doc.text(), "Test content");
}
#[test]
fn test_styles_written_and_readable() {
use crate::Document;
use std::io::Cursor;
let mut builder = DocumentBuilder::new();
builder.add_paragraph("Styled content");
let style = types::Style {
r#type: Some(types::STStyleType::Paragraph),
style_id: Some("MyHeading".to_string()),
name: Some(Box::new(types::CTString {
value: "My Heading".to_string(),
#[cfg(feature = "extra-attrs")]
extra_attrs: std::collections::HashMap::new(),
})),
..Default::default()
};
builder.add_style(style);
let mut buffer = Cursor::new(Vec::new());
builder.write(&mut buffer).unwrap();
buffer.set_position(0);
let doc = Document::from_reader(buffer).unwrap();
let styles = doc.styles();
assert_eq!(styles.style.len(), 1);
assert_eq!(styles.style[0].style_id.as_deref(), Some("MyHeading"));
}
}