use std::collections::{HashMap, HashSet};
use super::document_tree::{visit_document, visit_footer, visit_header, DocumentTreeVisitor};
use crate::{
Document, Footer, Footnote, FootnoteReference, Header, ImageIdAndBuf, ImageIdAndPath, Pic,
};
#[derive(Default)]
pub(crate) struct MediaRegistry {
media: Vec<ImageIdAndBuf>,
media_ids: HashSet<String>,
by_fingerprint: HashMap<(usize, u32), Vec<usize>>,
}
impl MediaRegistry {
fn register(&mut self, preferred_id: &str, bytes: Vec<u8>) -> String {
let fingerprint = (bytes.len(), crc32fast::hash(&bytes));
if let Some(index) = self
.by_fingerprint
.get(&fingerprint)
.and_then(|candidates| {
candidates
.iter()
.copied()
.find(|index| self.media[*index].1 == bytes)
})
{
return self.media[index].0.clone();
}
let media_id = self.unique_media_id(preferred_id);
let index = self.media.len();
self.media.push((media_id.clone(), bytes));
self.media_ids.insert(media_id.clone());
self.by_fingerprint
.entry(fingerprint)
.or_default()
.push(index);
media_id
}
pub(crate) fn into_media(self) -> Vec<ImageIdAndBuf> {
self.media
}
fn unique_media_id(&self, preferred_id: &str) -> String {
if !self.media_ids.contains(preferred_id) {
return preferred_id.to_owned();
}
(2usize..)
.map(|suffix| format!("{preferred_id}_{suffix}"))
.find(|candidate| !self.media_ids.contains(candidate))
.expect("the media ID space should not be exhausted")
}
}
pub(crate) struct CollectedPart {
pub(crate) relationships: Vec<ImageIdAndPath>,
pub(crate) footnotes: Vec<Footnote>,
}
struct PackagePartCollector<'a> {
registry: &'a mut MediaRegistry,
relationship_prefix: Option<&'a str>,
relationships: Vec<ImageIdAndPath>,
relationship_ids: HashSet<String>,
relationships_by_target: HashMap<String, String>,
footnotes: Vec<Footnote>,
}
impl<'a> PackagePartCollector<'a> {
fn new(registry: &'a mut MediaRegistry, relationship_prefix: Option<&'a str>) -> Self {
Self {
registry,
relationship_prefix,
relationships: Vec::new(),
relationship_ids: HashSet::new(),
relationships_by_target: HashMap::new(),
footnotes: Vec::new(),
}
}
fn finish(self) -> CollectedPart {
CollectedPart {
relationships: self.relationships,
footnotes: self.footnotes,
}
}
fn relationship_id(&self, picture_id: &str) -> String {
match self.relationship_prefix {
Some(prefix) => format!("{prefix}{picture_id}"),
None => picture_id.to_owned(),
}
}
fn unique_relationship_id(&self, preferred_id: &str) -> String {
match self.relationship_ids.contains(preferred_id) {
false => preferred_id.to_owned(),
true => (2usize..)
.map(|suffix| format!("{preferred_id}_{suffix}"))
.find(|candidate| !self.relationship_ids.contains(candidate))
.expect("the relationship ID space should not be exhausted"),
}
}
}
impl DocumentTreeVisitor for PackagePartCollector<'_> {
fn visit_picture(&mut self, picture: &mut Pic) {
let preferred_relationship_id = self.relationship_id(&picture.id);
let media_id = self.registry.register(
&preferred_relationship_id,
std::mem::take(&mut picture.image),
);
let target = format!("media/{media_id}.png");
if let Some(relationship_id) = self.relationships_by_target.get(&target) {
picture.id.clone_from(relationship_id);
return;
}
let relationship_id = self.unique_relationship_id(&preferred_relationship_id);
self.relationship_ids.insert(relationship_id.clone());
self.relationships_by_target
.insert(target.clone(), relationship_id.clone());
self.relationships.push((relationship_id.clone(), target));
picture.id = relationship_id;
}
fn visit_footnote_reference(&mut self, reference: &FootnoteReference) {
self.footnotes.push(reference.into());
}
}
pub(crate) fn collect_document_part(
document: &mut Document,
registry: &mut MediaRegistry,
) -> CollectedPart {
let mut collector = PackagePartCollector::new(registry, None);
visit_document(document, &mut collector);
collector.finish()
}
pub(crate) fn collect_document_footnotes(document: &mut Document) -> Vec<Footnote> {
#[derive(Default)]
struct FootnoteCollector {
footnotes: Vec<Footnote>,
}
impl DocumentTreeVisitor for FootnoteCollector {
fn visit_footnote_reference(&mut self, reference: &FootnoteReference) {
self.footnotes.push(reference.into());
}
}
let mut collector = FootnoteCollector::default();
visit_document(document, &mut collector);
collector.footnotes
}
pub(crate) fn collect_header_part(
header: &mut Header,
registry: &mut MediaRegistry,
) -> CollectedPart {
let mut collector = PackagePartCollector::new(registry, Some("header"));
visit_header(header, &mut collector);
collector.finish()
}
pub(crate) fn collect_footer_part(
footer: &mut Footer,
registry: &mut MediaRegistry,
) -> CollectedPart {
let mut collector = PackagePartCollector::new(registry, Some("footer"));
visit_footer(footer, &mut collector);
collector.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_deduplicates_identical_media_but_keeps_part_relationships() {
let bytes = vec![1, 2, 3, 4, 5];
let mut first =
Header::new().add_paragraph(crate::Paragraph::new().add_run(
crate::Run::new().add_image(Pic::new_with_dimensions(bytes.clone(), 1, 1)),
));
let mut second = Header::new().add_paragraph(
crate::Paragraph::new()
.add_run(crate::Run::new().add_image(Pic::new_with_dimensions(bytes, 1, 1))),
);
let mut registry = MediaRegistry::default();
let first = collect_header_part(&mut first, &mut registry);
let second = collect_header_part(&mut second, &mut registry);
assert_eq!(registry.media.len(), 1);
assert_eq!(first.relationships.len(), 1);
assert_eq!(second.relationships.len(), 1);
assert_eq!(first.relationships[0].1, second.relationships[0].1);
}
#[test]
fn identical_media_reuses_one_relationship_within_a_part() {
let bytes = vec![9, 8, 7, 6];
let mut first = Pic::new_with_dimensions(bytes.clone(), 1, 1);
first.id = "first".to_owned();
let mut second = Pic::new_with_dimensions(bytes, 1, 1);
second.id = "second".to_owned();
let mut document = Document::new()
.add_paragraph(crate::Paragraph::new().add_run(crate::Run::new().add_image(first)))
.add_paragraph(crate::Paragraph::new().add_run(crate::Run::new().add_image(second)));
let mut registry = MediaRegistry::default();
let part = collect_document_part(&mut document, &mut registry);
assert_eq!(registry.media.len(), 1);
assert_eq!(part.relationships.len(), 1);
}
}