easydoc_reader/extractor/
image.rs1use std::borrow::Cow;
7use std::collections::HashMap;
8use std::io::Read;
9
10use easydoc_core::{DocError, Result};
11use quick_xml::Reader as XmlReader;
12use quick_xml::events::Event;
13
14pub struct Relationships {
21 rels: HashMap<String, String>,
23 hyperlinks: HashMap<String, String>,
25}
26
27enum RelType {
29 Image,
30 Hyperlink,
31 Other,
32}
33
34impl Relationships {
35 pub fn parse(rels_xml: &str) -> Result<Self> {
53 let mut rels = HashMap::new();
54 let mut hyperlinks = HashMap::new();
55 let mut reader = XmlReader::from_reader(rels_xml.as_bytes());
56 reader.config_mut().trim_text(true);
57 let mut buf = Vec::new();
58
59 loop {
60 match reader.read_event_into(&mut buf) {
61 Ok(Event::Eof) => break,
62 Ok(Event::Empty(ref tag)) => {
63 let name = tag.name();
64 if name.as_ref() == b"Relationship" {
65 let mut id = None;
66 let mut target = None;
67 let mut rel_type = RelType::Other;
68
69 for attr in tag.attributes().flatten() {
70 match attr.key.as_ref() {
71 b"Id" => {
72 id = attr
73 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
74 .ok()
75 .map(Cow::into_owned);
76 }
77 b"Target" => {
78 target = attr
79 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
80 .ok()
81 .map(Cow::into_owned);
82 }
83 b"Type" => {
84 if let Ok(val) =
85 attr.normalized_value(quick_xml::XmlVersion::Implicit1_0)
86 {
87 if val.ends_with("/image") {
88 rel_type = RelType::Image;
89 } else if val.ends_with("/hyperlink") {
90 rel_type = RelType::Hyperlink;
91 }
92 }
93 }
94 _ => {}
95 }
96 }
97
98 if let (Some(id), Some(target)) = (id, target) {
99 match rel_type {
100 RelType::Image => {
101 let full_path = if target.starts_with("media/") {
104 format!("word/{target}")
105 } else {
106 target
107 };
108 rels.insert(id, full_path);
109 }
110 RelType::Hyperlink => {
111 hyperlinks.insert(id, target);
114 }
115 RelType::Other => {}
116 }
117 }
118 }
119 }
120 Err(e) => {
121 return Err(DocError::Format(format!(
122 "XML parse error in relationships: {e}"
123 )));
124 }
125 _ => {}
126 }
127 buf.clear();
128 }
129
130 Ok(Self { rels, hyperlinks })
131 }
132
133 #[must_use]
139 pub fn resolve(&self, rel_id: &str) -> Option<&str> {
140 self.resolve_image(rel_id)
141 }
142
143 #[must_use]
148 pub fn resolve_image(&self, rel_id: &str) -> Option<&str> {
149 self.rels.get(rel_id).map(String::as_str)
150 }
151
152 #[must_use]
157 pub fn resolve_hyperlink(&self, rel_id: &str) -> Option<&str> {
158 self.hyperlinks.get(rel_id).map(String::as_str)
159 }
160
161 #[must_use]
163 pub fn len(&self) -> usize {
164 self.rels.len() + self.hyperlinks.len()
165 }
166
167 #[must_use]
169 pub fn is_empty(&self) -> bool {
170 self.rels.is_empty() && self.hyperlinks.is_empty()
171 }
172}
173
174pub fn read_zip_part<R: Read + std::io::Seek>(
180 archive: &mut zip::ZipArchive<R>,
181 part_name: &str,
182) -> Result<Vec<u8>> {
183 let mut file = archive
184 .by_name(part_name)
185 .map_err(|e| DocError::Zip(format!("entry '{part_name}' not found: {e}")))?;
186 let mut buf = Vec::new();
187 file.read_to_end(&mut buf)?;
188 Ok(buf)
189}
190
191#[must_use]
195pub fn extension_from_filename(name: &str) -> Option<String> {
196 std::path::Path::new(name)
197 .extension()
198 .and_then(|s| s.to_str())
199 .map(str::to_lowercase)
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn parse_typical_rels() {
208 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
209<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
210 <Relationship Id="rId1" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"/>
211 <Relationship Id="rId5" Target="media/image1.png" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"/>
212 <Relationship Id="rId6" Target="media/image2.jpeg" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"/>
213</Relationships>"#;
214
215 let rels = Relationships::parse(xml).unwrap();
216 assert_eq!(rels.len(), 2);
217 assert_eq!(rels.resolve("rId5"), Some("word/media/image1.png"));
218 assert_eq!(rels.resolve("rId6"), Some("word/media/image2.jpeg"));
219 assert_eq!(rels.resolve("rId1"), None);
221 }
222
223 #[test]
224 fn parse_empty_rels() {
225 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
226<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
227</Relationships>"#;
228
229 let rels = Relationships::parse(xml).unwrap();
230 assert!(rels.is_empty());
231 }
232
233 #[test]
234 fn resolve_unknown_relid() {
235 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
236<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
237</Relationships>"#;
238
239 let rels = Relationships::parse(xml).unwrap();
240 assert_eq!(rels.resolve("rId999"), None);
241 }
242
243 #[test]
244 fn extension_from_filename_png() {
245 assert_eq!(
246 extension_from_filename("image1.png"),
247 Some("png".to_owned())
248 );
249 }
250
251 #[test]
252 fn extension_from_filename_jpeg() {
253 assert_eq!(
254 extension_from_filename("photo.jpeg"),
255 Some("jpeg".to_owned())
256 );
257 }
258
259 #[test]
260 fn extension_from_filename_uppercase() {
261 assert_eq!(
262 extension_from_filename("image1.PNG"),
263 Some("png".to_owned())
264 );
265 }
266
267 #[test]
268 fn extension_from_filename_no_extension() {
269 assert_eq!(extension_from_filename("README"), None);
270 }
271
272 #[test]
273 fn extension_from_filename_dotfile() {
274 assert_eq!(extension_from_filename(".gitignore"), None);
275 }
276
277 #[test]
278 fn relationships_includes_hyperlinks() {
279 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
280<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
281 <Relationship Id="rId1" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"/>
282 <Relationship Id="rId5" Target="media/image1.png" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"/>
283 <Relationship Id="rId10" Target="https://example.com" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" TargetMode="External"/>
284</Relationships>"#;
285
286 let rels = Relationships::parse(xml).unwrap();
287 assert_eq!(rels.len(), 2);
289 assert_eq!(rels.resolve_image("rId5"), Some("word/media/image1.png"));
290 assert_eq!(rels.resolve_hyperlink("rId10"), Some("https://example.com"));
291 assert_eq!(rels.resolve_hyperlink("rId999"), None);
293 assert_eq!(rels.resolve_image("rId10"), None);
294 }
295
296 #[test]
297 fn relationships_external_target_mode() {
298 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
299<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
300 <Relationship Id="rId3" Target="https://rust-lang.org" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" TargetMode="External"/>
301 <Relationship Id="rId4" Target="mailto:user@example.com" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" TargetMode="External"/>
302</Relationships>"#;
303
304 let rels = Relationships::parse(xml).unwrap();
305 assert_eq!(
306 rels.resolve_hyperlink("rId3"),
307 Some("https://rust-lang.org")
308 );
309 assert_eq!(
310 rels.resolve_hyperlink("rId4"),
311 Some("mailto:user@example.com")
312 );
313 assert_eq!(rels.resolve("rId3"), None);
315 }
316
317 #[test]
318 fn relationships_backward_compat_resolve_is_alias() {
319 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
320<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
321 <Relationship Id="rId5" Target="media/image1.png" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"/>
322</Relationships>"#;
323
324 let rels = Relationships::parse(xml).unwrap();
325 assert_eq!(rels.resolve("rId5"), rels.resolve_image("rId5"));
327 }
328}