1use std::path::Path;
4
5use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
6use quick_xml::Writer;
7
8use crate::error::Result;
9use crate::header::{Header, StructuralHints};
10use crate::reader::SIGNATURE;
11use crate::value::Value;
12
13const OFFSET_WIDTH: usize = 12;
16
17impl Header {
18 #[must_use]
37 pub fn to_header_bytes(&self, hints: &StructuralHints) -> Vec<u8> {
38 let size = data_size(hints);
39
40 let placeholder = "0".repeat(OFFSET_WIDTH);
44 let xml_len = self.render_xml(hints, &placeholder, size).len();
45 let offset = 16 + xml_len;
46 let offset_str = format!("{offset:0width$}", width = OFFSET_WIDTH);
47 let xml = self.render_xml(hints, &offset_str, size);
48 debug_assert_eq!(xml.len(), xml_len, "offset width must not change length");
49
50 let mut out = Vec::with_capacity(16 + xml.len());
51 out.extend_from_slice(SIGNATURE);
52 out.extend_from_slice(&u32::try_from(xml.len()).unwrap_or(u32::MAX).to_le_bytes());
53 out.extend_from_slice(&[0u8; 4]); out.extend_from_slice(&xml);
55 out
56 }
57
58 pub fn update_file<P: AsRef<Path>>(
109 path: P,
110 edit: impl FnOnce(&mut Self) -> Result<()>,
111 ) -> Result<()> {
112 crate::splice::update_file(path, edit)
113 }
114
115 fn render_xml(&self, hints: &StructuralHints, offset_str: &str, size: usize) -> Vec<u8> {
117 const INFALLIBLE: &str = "writing XML to an in-memory buffer cannot fail";
118
119 let mut w = Writer::new(Vec::new());
120 w.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
121 .expect(INFALLIBLE);
122
123 let mut xisf = BytesStart::new("xisf");
124 xisf.push_attribute(("version", "1.0"));
125 xisf.push_attribute(("xmlns", "http://www.pixinsight.com/xisf"));
126 w.write_event(Event::Start(xisf)).expect(INFALLIBLE);
127
128 let mut image = BytesStart::new("Image");
129 image.push_attribute(("geometry", hints.geometry.as_str()));
130 image.push_attribute(("sampleFormat", hints.sample_format.as_str()));
131 image.push_attribute(("colorSpace", hints.color_space.as_str()));
132 let location = format!("attachment:{offset_str}:{size}");
133 image.push_attribute(("location", location.as_str()));
134 w.write_event(Event::Start(image)).expect(INFALLIBLE);
135
136 for kw in &self.keywords {
137 let mut e = BytesStart::new("FITSKeyword");
138 e.push_attribute(("name", kw.name.as_str()));
139 let value = match &kw.value {
140 Value::Str(s) => format!("'{s}'"),
141 Value::Literal(s) => s.clone(),
142 };
143 e.push_attribute(("value", value.as_str()));
144 e.push_attribute(("comment", kw.comment.as_str()));
145 w.write_event(Event::Empty(e)).expect(INFALLIBLE);
146 }
147
148 for (id, p) in &self.properties {
149 let mut e = BytesStart::new("Property");
150 e.push_attribute(("id", id.as_str()));
151 e.push_attribute(("type", p.type_.as_str()));
152 e.push_attribute(("value", p.value.as_str()));
153 if !p.format.is_empty() {
154 e.push_attribute(("format", p.format.as_str()));
155 }
156 if !p.comment.is_empty() {
157 e.push_attribute(("comment", p.comment.as_str()));
158 }
159 w.write_event(Event::Empty(e)).expect(INFALLIBLE);
160 }
161
162 w.write_event(Event::End(BytesEnd::new("Image")))
163 .expect(INFALLIBLE);
164 w.write_event(Event::End(BytesEnd::new("xisf")))
165 .expect(INFALLIBLE);
166
167 w.into_inner()
168 }
169}
170
171fn data_size(hints: &StructuralHints) -> usize {
173 let samples: Option<usize> = hints
174 .geometry
175 .split(':')
176 .map(|d| d.trim().parse::<usize>().ok())
177 .collect::<Option<Vec<_>>>()
178 .map(|dims| dims.iter().product());
179 let samples = samples.filter(|&s| s > 0).unwrap_or(1);
180 samples
181 .saturating_mul(bytes_per_sample(&hints.sample_format))
182 .max(1)
183}
184
185fn bytes_per_sample(format: &str) -> usize {
187 match format {
188 "UInt16" | "Int16" => 2,
189 "UInt32" | "Int32" | "Float32" => 4,
190 "UInt64" | "Int64" | "Float64" | "Complex32" => 8,
191 "Complex64" => 16,
192 _ => 1, }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn hints(geometry: &str, sample_format: &str) -> StructuralHints {
201 StructuralHints {
202 geometry: geometry.to_owned(),
203 sample_format: sample_format.to_owned(),
204 color_space: "Gray".to_owned(),
205 }
206 }
207
208 #[test]
209 fn bytes_per_sample_matrix() {
210 for (format, bytes) in [
211 ("UInt8", 1),
212 ("Int8", 1),
213 ("UInt16", 2),
214 ("Int16", 2),
215 ("UInt32", 4),
216 ("Int32", 4),
217 ("Float32", 4),
218 ("UInt64", 8),
219 ("Int64", 8),
220 ("Float64", 8),
221 ("Complex32", 8),
222 ("Complex64", 16),
223 ("SomethingElse", 1),
224 ] {
225 assert_eq!(bytes_per_sample(format), bytes, "{format}");
226 }
227 }
228
229 #[test]
230 fn data_size_from_geometry() {
231 assert_eq!(data_size(&hints("1:1:1", "UInt8")), 1);
232 assert_eq!(data_size(&hints("100:100:3", "Float32")), 120_000);
233 assert_eq!(data_size(&hints("16:16:1", "UInt16")), 512);
234 assert_eq!(data_size(&hints("abc", "UInt8")), 1);
236 assert_eq!(data_size(&hints("0:0:0", "Float32")), 4);
237 assert_eq!(data_size(&hints("", "UInt8")), 1);
238 }
239
240 #[test]
241 fn header_only_output_has_no_data_block() {
242 let h = Header::new();
243 let hints = StructuralHints::default();
244 let bytes = h.to_header_bytes(&hints);
245 let xml_len = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
246 assert_eq!(bytes.len(), 16 + xml_len);
247 }
248
249 #[test]
250 fn attachment_offset_is_fixed_width_and_correct() {
251 let h = Header::new();
252 let bytes = h.to_header_bytes(&StructuralHints::default());
253 let xml_len = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
254 let xml = std::str::from_utf8(&bytes[16..16 + xml_len]).unwrap();
255 let offset = format!("{:0width$}", 16 + xml_len, width = OFFSET_WIDTH);
256 assert!(
257 xml.contains(&format!("attachment:{offset}:")),
258 "location must point right past the header: {xml}"
259 );
260 }
261
262 #[test]
263 fn xml_special_characters_round_trip() {
264 let mut h = Header::new();
265 h.set("OBJECT", "a<b&\"c'd").unwrap();
266 h.set_comment("OBJECT", "less < & \"quoted\"").unwrap();
267 h.set_property("Notes:Text", "x<y&z").unwrap();
268 let parsed = Header::parse(&h.to_header_bytes(&StructuralHints::default())).unwrap();
269 assert_eq!(parsed, h);
270 assert_eq!(parsed.get_str("OBJECT").unwrap(), Some("a<b&\"c'd"));
271 assert_eq!(parsed.property("Notes:Text"), Some("x<y&z"));
272 }
273
274 #[test]
275 fn empty_header_round_trips() {
276 let h = Header::new();
277 let parsed = Header::parse(&h.to_header_bytes(&StructuralHints::default())).unwrap();
278 assert_eq!(parsed, h);
279 }
280}