1use std::fmt::Write as _;
9
10fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
11 data.get(offset..offset + 4)
12 .map(|bytes| u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
13}
14
15fn read_i32(data: &[u8], offset: usize) -> Option<i32> {
16 read_u32(data, offset).map(|value| value as i32)
17}
18
19fn read_i16(data: &[u8], offset: usize) -> Option<i16> {
20 data.get(offset..offset + 2)
21 .map(|bytes| i16::from_le_bytes([bytes[0], bytes[1]]))
22}
23
24fn escape_svg(value: &str) -> String {
25 value
26 .replace('&', "&")
27 .replace('<', "<")
28 .replace('>', ">")
29 .replace('"', """)
30}
31
32fn colorref_to_css(color: u32) -> String {
33 let red = color & 0xff;
34 let green = (color >> 8) & 0xff;
35 let blue = (color >> 16) & 0xff;
36 format!("#{red:02x}{green:02x}{blue:02x}")
37}
38
39#[derive(Clone, Debug, PartialEq)]
40enum DeviceObject {
41 SolidBrush(u32),
42 NullBrush,
43 SolidPen { color: u32, width_logical: f64 },
44 NullPen,
45 Font(LogicalFont),
46}
47
48#[derive(Clone, Debug, PartialEq)]
49struct LogicalFont {
50 family: String,
51 height: f64,
52 weight: u32,
53 italic: bool,
54 underline: bool,
55 strikeout: bool,
56}
57
58#[derive(Clone)]
59struct SavedState {
60 mapping: Mapping,
61 brush: Option<u32>,
62 pen: Option<(u32, f64)>,
63 font: Option<LogicalFont>,
64 text_color: u32,
65 background_color: u32,
66 background_mode: u32,
67 text_align: u32,
68 fill_rule: &'static str,
69 current_point: (f64, f64),
70 clip: Option<usize>,
71}
72
73#[derive(Clone, Copy, Debug)]
74struct Mapping {
75 window_org: (f64, f64),
76 window_ext: (f64, f64),
77 viewport_org: (f64, f64),
78 viewport_ext: (f64, f64),
79}
80
81impl Mapping {
82 fn identity() -> Self {
83 Mapping {
84 window_org: (0.0, 0.0),
85 window_ext: (1.0, 1.0),
86 viewport_org: (0.0, 0.0),
87 viewport_ext: (1.0, 1.0),
88 }
89 }
90
91 fn scale_x(&self) -> f64 {
92 if self.window_ext.0 == 0.0 {
93 1.0
94 } else {
95 self.viewport_ext.0 / self.window_ext.0
96 }
97 }
98
99 fn scale_y(&self) -> f64 {
100 if self.window_ext.1 == 0.0 {
101 1.0
102 } else {
103 self.viewport_ext.1 / self.window_ext.1
104 }
105 }
106
107 fn map(&self, x: f64, y: f64) -> (f64, f64) {
108 (
109 (x - self.window_org.0) * self.scale_x() + self.viewport_org.0,
110 (y - self.window_org.1) * self.scale_y() + self.viewport_org.1,
111 )
112 }
113}
114
115struct Coordinate(f64);
116
117impl std::fmt::Display for Coordinate {
118 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 let scaled = (self.0 * 100.0).round();
120 let rounded = scaled / 100.0;
121 if rounded == rounded.trunc() {
122 write!(formatter, "{}", rounded as i64)
123 } else if scaled.abs() <= (1u64 << 40) as f64 {
124 let value = scaled as i64;
126 let absolute = value.unsigned_abs();
127 if value < 0 {
128 formatter.write_str("-")?;
129 }
130 write!(formatter, "{}.", absolute / 100)?;
131 let fraction = absolute % 100;
132 if fraction < 10 {
133 write!(formatter, "0{fraction}")
134 } else if fraction % 10 == 0 {
135 write!(formatter, "{}", fraction / 10)
136 } else {
137 write!(formatter, "{fraction}")
138 }
139 } else {
140 write!(formatter, "{rounded}")
141 }
142 }
143}
144
145fn fmt(value: f64) -> Coordinate {
146 Coordinate(value)
147}
148
149const EMR_HEADER: u32 = 1;
150const EMR_POLYGON16: u32 = 86;
151const EMR_POLYLINE16: u32 = 87;
152const EMR_POLYBEZIER16: u32 = 85;
153const EMR_POLYBEZIERTO16: u32 = 88;
154const EMR_POLYLINETO16: u32 = 89;
155const EMR_POLYPOLYLINE16: u32 = 90;
156const EMR_POLYPOLYGON16: u32 = 91;
157const EMR_SETWINDOWEXTEX: u32 = 9;
158const EMR_SETWINDOWORGEX: u32 = 10;
159const EMR_SETVIEWPORTEXTEX: u32 = 11;
160const EMR_SETVIEWPORTORGEX: u32 = 12;
161const EMR_SETBRUSHORGEX: u32 = 13;
162const EMR_EOF: u32 = 14;
163const EMR_SETMAPMODE: u32 = 17;
164const EMR_SETBKMODE: u32 = 18;
165const EMR_SETPOLYFILLMODE: u32 = 19;
166const EMR_SETROP2: u32 = 20;
167const EMR_SETSTRETCHBLTMODE: u32 = 21;
168const EMR_SETTEXTALIGN: u32 = 22;
169const EMR_SETTEXTCOLOR: u32 = 24;
170const EMR_SETBKCOLOR: u32 = 25;
171const EMR_MOVETOEX: u32 = 27;
172const EMR_SETMETARGN: u32 = 28;
173const EMR_SETMITERLIMIT: u32 = 58;
174const EMR_LINETO: u32 = 54;
175const EMR_SELECTOBJECT: u32 = 37;
176const EMR_CREATEPEN: u32 = 38;
177const EMR_CREATEBRUSHINDIRECT: u32 = 39;
178const EMR_DELETEOBJECT: u32 = 40;
179const EMR_BEGINPATH: u32 = 59;
180const EMR_ENDPATH: u32 = 60;
181const EMR_CLOSEFIGURE: u32 = 61;
182const EMR_FILLPATH: u32 = 62;
183const EMR_STROKEANDFILLPATH: u32 = 63;
184const EMR_STROKEPATH: u32 = 64;
185const EMR_SELECTCLIPPATH: u32 = 67;
186const EMR_COMMENT: u32 = 70;
187const EMR_SAVEDC: u32 = 33;
188const EMR_RESTOREDC: u32 = 34;
189const EMR_INTERSECTCLIPRECT: u32 = 30;
190const EMR_BITBLT: u32 = 76;
191const EMR_EXTCREATEFONTINDIRECTW: u32 = 82;
192const EMR_EXTTEXTOUTW: u32 = 84;
193const EMR_EXTSELECTCLIPRGN: u32 = 75;
194const EMR_EXTCREATEPEN: u32 = 95;
195const EMR_SETICMMODE: u32 = 98;
196
197const STOCK_OBJECT_FLAG: u32 = 0x8000_0000;
198
199struct Converter<'a> {
200 data: &'a [u8],
201 mapping: Mapping,
202 objects: std::collections::HashMap<u32, DeviceObject>,
203 current_brush: Option<u32>,
204 current_pen: Option<(u32, f64)>,
205 current_font: Option<LogicalFont>,
206 text_color: u32,
207 background_color: u32,
208 background_mode: u32,
209 text_align: u32,
210 saved_states: Vec<SavedState>,
211 fill_rule: &'static str,
212 in_path: bool,
213 path_data: String,
214 pending_path: Option<String>,
215 current_point: (f64, f64),
216 elements: Vec<String>,
217 clip_defs: Vec<String>,
218 active_clip_id: Option<usize>,
219 bounds: (i32, i32, i32, i32),
220}
221
222impl<'a> Converter<'a> {
223 fn save_state(&self) -> SavedState {
224 SavedState {
225 mapping: self.mapping,
226 brush: self.current_brush,
227 pen: self.current_pen,
228 font: self.current_font.clone(),
229 text_color: self.text_color,
230 background_color: self.background_color,
231 background_mode: self.background_mode,
232 text_align: self.text_align,
233 fill_rule: self.fill_rule,
234 current_point: self.current_point,
235 clip: self.active_clip_id,
236 }
237 }
238
239 fn restore_state(&mut self, state: SavedState) {
240 self.mapping = state.mapping;
241 self.current_brush = state.brush;
242 self.current_pen = state.pen;
243 self.current_font = state.font;
244 self.text_color = state.text_color;
245 self.background_color = state.background_color;
246 self.background_mode = state.background_mode;
247 self.text_align = state.text_align;
248 self.fill_rule = state.fill_rule;
249 self.current_point = state.current_point;
250 self.active_clip_id = state.clip;
251 }
252
253 fn mapped_rect(&self, x: f64, y: f64, width: f64, height: f64) -> String {
254 let first = self.map_point(x, y);
255 let last = self.map_point(x + width, y + height);
256 format!(
257 "x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"",
258 fmt(first.0.min(last.0)),
259 fmt(first.1.min(last.1)),
260 fmt((last.0 - first.0).abs()),
261 fmt((last.1 - first.1).abs())
262 )
263 }
264
265 fn intersect_clip_rect(&mut self, rect: String) {
266 let id = self.clip_defs.len();
267 let previous = self
268 .active_clip_id
269 .map(|id| format!(" clip-path=\"url(#c{id})\""))
270 .unwrap_or_default();
271 self.clip_defs.push(format!(
272 "<clipPath id=\"c{id}\"><rect {rect}{previous}/></clipPath>"
273 ));
274 self.active_clip_id = Some(id);
275 }
276
277 fn emit_text(&mut self, record: &[u8]) -> Option<()> {
278 if record.len() < 76 || self.in_path {
279 return None;
280 }
281 let font = self.current_font.clone()?;
282 let count = read_u32(record, 44)? as usize;
283 let string_offset = read_u32(record, 48)? as usize;
284 let options = read_u32(record, 52)?;
285 let dx_offset = read_u32(record, 72)? as usize;
286 if count > 0 && string_offset < 76 {
287 return None;
288 }
289 if options & !(2 | 4 | 0x1000 | 0x2000) != 0 || self.text_align & 1 != 0 {
290 return None;
291 }
292 let encoded =
293 record.get(string_offset..string_offset.checked_add(count.checked_mul(2)?)?)?;
294 let units: Vec<u16> = encoded
295 .chunks_exact(2)
296 .map(|v| u16::from_le_bytes([v[0], v[1]]))
297 .collect();
298 let text = String::from_utf16(&units).ok()?;
299 if text.is_empty() {
300 return Some(());
301 }
302 let x = read_i32(record, 36)? as f64;
303 let y = read_i32(record, 40)? as f64;
304 let mut advances = Vec::new();
305 if dx_offset != 0 {
306 let stride = if options & 0x2000 != 0 { 8 } else { 4 };
307 if dx_offset < 76 || dx_offset.checked_add(count.checked_mul(stride)?)? > record.len() {
308 return None;
309 }
310 for index in 0..count {
311 let offset = dx_offset + index * stride;
312 advances.push((
313 read_i32(record, offset)? as f64,
314 if stride == 8 {
315 read_i32(record, offset + 4)? as f64
316 } else {
317 0.0
318 },
319 ));
320 }
321 }
322 let old_clip = self.active_clip_id;
323 let rectangle = self.mapped_rect(
324 read_i32(record, 56)? as f64,
325 read_i32(record, 60)? as f64,
326 (read_i32(record, 64)? as f64) - (read_i32(record, 56)? as f64),
327 (read_i32(record, 68)? as f64) - (read_i32(record, 60)? as f64),
328 );
329 if options & 4 != 0 {
330 self.intersect_clip_rect(rectangle.clone());
331 }
332 if options & 2 != 0 {
333 self.push_element(format!(
334 "<rect {rectangle} fill=\"{}\"/>",
335 colorref_to_css(self.background_color)
336 ));
337 } else if self.background_mode == 2 {
338 let rect = self.mapped_rect(
339 read_i32(record, 8)? as f64,
340 read_i32(record, 12)? as f64,
341 (read_i32(record, 16)? as f64) - (read_i32(record, 8)? as f64) + 1.0,
342 (read_i32(record, 20)? as f64) - (read_i32(record, 12)? as f64) + 1.0,
343 );
344 self.push_element(format!(
345 "<rect {rect} fill=\"{}\"/>",
346 colorref_to_css(self.background_color)
347 ));
348 }
349 let horizontal = self.text_align & 6;
350 let width: f64 = advances.iter().map(|v| v.0).sum();
351 let shift = if horizontal == 6 {
352 width / 2.0
353 } else if horizontal == 2 {
354 width
355 } else {
356 0.0
357 };
358 let coordinate_capacity = if advances.is_empty() { 16 } else { count.saturating_mul(6) };
359 let mut xs = String::with_capacity(coordinate_capacity);
360 let mut ys = String::with_capacity(coordinate_capacity);
361 let mut position = (x - shift, y);
362 let mut unit_index = 0;
363 if advances.is_empty() {
364 let point = self.map_point(x, y);
365 let _ = write!(xs, "{}", fmt(point.0));
366 let _ = write!(ys, "{}", fmt(point.1));
367 } else {
368 for character in text.chars() {
369 let point = self.map_point(position.0, position.1);
370 if !xs.is_empty() {
371 xs.push(' ');
372 ys.push(' ');
373 }
374 let _ = write!(xs, "{}", fmt(point.0));
375 let _ = write!(ys, "{}", fmt(point.1));
376 for _ in 0..character.len_utf16() {
377 position.0 += advances[unit_index].0;
378 position.1 += advances[unit_index].1;
379 unit_index += 1;
380 }
381 }
382 }
383 let anchor = if advances.is_empty() && horizontal == 6 {
384 "middle"
385 } else if advances.is_empty() && horizontal == 2 {
386 "end"
387 } else {
388 "start"
389 };
390 let baseline = match self.text_align & 24 {
391 24 => "alphabetic",
392 8 => "text-after-edge",
393 _ => "text-before-edge",
394 };
395 let decoration = [
396 if font.underline { "underline" } else { "" },
397 if font.strikeout { "line-through" } else { "" },
398 ]
399 .join(" ");
400 self.push_element(format!(
401 "<text xml:space=\"preserve\" x=\"{}\" y=\"{}\" font-family=\"{}\" font-size=\"{}\" font-weight=\"{}\" font-style=\"{}\" text-decoration=\"{}\" text-anchor=\"{anchor}\" dominant-baseline=\"{baseline}\" fill=\"{}\">{}</text>",
402 xs,ys,escape_svg(&font.family),fmt(font.height*self.mapping.scale_y().abs()),font.weight,
403 if font.italic { "italic" } else { "normal" },decoration.trim(),colorref_to_css(self.text_color),escape_svg(&text)));
404 self.active_clip_id = old_clip;
405 Some(())
406 }
407 fn map_point(&self, x: f64, y: f64) -> (f64, f64) {
408 self.mapping.map(x, y)
409 }
410
411 fn push_element(&mut self, element: String) {
412 if let Some(clip_id) = self.active_clip_id {
413 self.elements
414 .push(format!("<g clip-path=\"url(#c{clip_id})\">{element}</g>"));
415 } else {
416 self.elements.push(element);
417 }
418 }
419
420 fn fill_attr(&self) -> String {
421 match self.current_brush {
422 Some(color) => format!("fill=\"{}\"", colorref_to_css(color)),
423 None => "fill=\"none\"".to_string(),
424 }
425 }
426
427 fn stroke_attr(&self) -> String {
428 match self.current_pen {
429 Some((color, width_logical)) => {
430 let width_px = (width_logical * self.mapping.scale_x()).abs().max(0.25);
431 format!(
432 "stroke=\"{}\" stroke-width=\"{}\"",
433 colorref_to_css(color),
434 fmt(width_px)
435 )
436 }
437 None => "stroke=\"none\"".to_string(),
438 }
439 }
440
441 fn emit_path(&mut self, d: String, fill: bool, stroke: bool) {
442 if d.is_empty() {
443 return;
444 }
445 let fill_attr = if fill {
446 format!("{} fill-rule=\"{}\"", self.fill_attr(), self.fill_rule)
447 } else {
448 "fill=\"none\"".to_string()
449 };
450 let stroke_attr = if stroke {
451 self.stroke_attr()
452 } else {
453 "stroke=\"none\"".to_string()
454 };
455 self.push_element(format!("<path d=\"{d}\" {fill_attr} {stroke_attr}/>"));
456 }
457
458 fn read_points16(&self, offset: usize, record_end: usize) -> Option<Vec<(f64, f64)>> {
459 let count = read_u32(self.data, offset)? as usize;
460 let mut points = Vec::with_capacity(count);
461 let mut cursor = offset + 4;
462 for _ in 0..count {
463 if cursor + 4 > record_end {
464 return None;
465 }
466 let x = read_i16(self.data, cursor)? as f64;
467 let y = read_i16(self.data, cursor + 2)? as f64;
468 points.push(self.map_point(x, y));
469 cursor += 4;
470 }
471 Some(points)
472 }
473
474 fn polyline_to_d(points: &[(f64, f64)], close: bool) -> String {
475 let mut d = String::new();
476 for (index, (x, y)) in points.iter().enumerate() {
477 if index == 0 {
478 write!(d, "M{} {}", fmt(*x), fmt(*y)).expect("writing into a string cannot fail");
479 } else {
480 write!(d, "L{} {}", fmt(*x), fmt(*y)).expect("writing into a string cannot fail");
481 }
482 }
483 if close && !d.is_empty() {
484 d.push('Z');
485 }
486 d
487 }
488
489 fn bezier_to_d(points: &[(f64, f64)], starts_with_move: bool) -> Option<String> {
490 let mut d = String::new();
491 let control_points = if starts_with_move {
492 let Some(((x, y), rest)) = points.split_first() else {
493 return None;
494 };
495 write!(d, "M{} {}", fmt(*x), fmt(*y)).expect("writing into a string cannot fail");
496 rest
497 } else {
498 points
499 };
500 if control_points.len() % 3 != 0 {
501 return None;
502 }
503 for triple in control_points.chunks(3) {
504 write!(d,
505 "C{} {} {} {} {} {}",
506 fmt(triple[0].0),
507 fmt(triple[0].1),
508 fmt(triple[1].0),
509 fmt(triple[1].1),
510 fmt(triple[2].0),
511 fmt(triple[2].1)
512 ).expect("writing into a string cannot fail");
513 }
514 Some(d)
515 }
516}
517
518pub fn emf_to_svg(data: &[u8]) -> Option<String> {
522 if data.len() < 88 || read_u32(data, 0)? != EMR_HEADER {
523 return None;
524 }
525 let bounds = (
526 read_i32(data, 8)?,
527 read_i32(data, 12)?,
528 read_i32(data, 16)?,
529 read_i32(data, 20)?,
530 );
531 if bounds.2 <= bounds.0 || bounds.3 <= bounds.1 {
532 return None;
533 }
534
535 let mut converter = Converter {
536 data,
537 mapping: Mapping::identity(),
538 objects: std::collections::HashMap::new(),
539 current_brush: Some(0x00ff_ffff),
540 current_pen: Some((0, 1.0)),
541 current_font: None,
542 text_color: 0,
543 background_color: 0x00ff_ffff,
544 background_mode: 2,
545 text_align: 0,
546 saved_states: Vec::new(),
547 fill_rule: "evenodd",
548 in_path: false,
549 path_data: String::new(),
550 pending_path: None,
551 current_point: (0.0, 0.0),
552 elements: Vec::new(),
553 clip_defs: Vec::new(),
554 active_clip_id: None,
555 bounds,
556 };
557
558 let header_size = read_u32(data, 4)? as usize;
559 let mut offset = header_size;
560 while offset + 8 <= data.len() {
561 let record_type = read_u32(data, offset)?;
562 let record_size = read_u32(data, offset + 4)? as usize;
563 if record_size < 8 || offset + record_size > data.len() {
564 return None;
565 }
566 let record_end = offset + record_size;
567 let body = offset + 8;
568
569 match record_type {
570 EMR_EOF => break,
571 EMR_HEADER => return None,
572 EMR_SETMAPMODE
573 | EMR_SETSTRETCHBLTMODE
574 | EMR_SETBRUSHORGEX
575 | EMR_SETMETARGN
576 | EMR_SETMITERLIMIT
577 | EMR_SETICMMODE
578 | EMR_COMMENT => {}
579 EMR_SAVEDC => converter.saved_states.push(converter.save_state()),
580 EMR_RESTOREDC => {
581 let level = read_i32(data, body)?;
582 let index = if level < 0 {
583 converter
584 .saved_states
585 .len()
586 .checked_sub(level.unsigned_abs() as usize)?
587 } else {
588 (level as usize).checked_sub(1)?
589 };
590 let state = converter.saved_states.get(index)?.clone();
591 converter.saved_states.truncate(index);
592 converter.restore_state(state);
593 }
594 EMR_SETROP2 => {
595 if read_u32(data, body)? != 13 {
596 return None;
597 }
598 }
599 EMR_SETBKMODE => converter.background_mode = read_u32(data, body)?,
600 EMR_SETTEXTALIGN => converter.text_align = read_u32(data, body)?,
601 EMR_SETTEXTCOLOR => converter.text_color = read_u32(data, body)?,
602 EMR_SETBKCOLOR => converter.background_color = read_u32(data, body)?,
603 EMR_INTERSECTCLIPRECT => {
604 if record_size < 24 {
605 return None;
606 }
607 let left = read_i32(data, body)? as f64;
608 let top = read_i32(data, body + 4)? as f64;
609 let rect = converter.mapped_rect(
610 left,
611 top,
612 read_i32(data, body + 8)? as f64 - left,
613 read_i32(data, body + 12)? as f64 - top,
614 );
615 converter.intersect_clip_rect(rect);
616 }
617 EMR_BITBLT => {
618 if record_size < 100 {
619 return None;
620 }
621 let color = match read_u32(data, offset + 40)? {
622 0x00f00021 => converter.current_brush?,
623 0x00000042 => 0,
624 0x00ff0062 => 0x00ffffff,
625 _ => return None,
626 };
627 let rect = converter.mapped_rect(
628 read_i32(data, offset + 24)? as f64,
629 read_i32(data, offset + 28)? as f64,
630 read_i32(data, offset + 32)? as f64,
631 read_i32(data, offset + 36)? as f64,
632 );
633 converter.push_element(format!(
634 "<rect {rect} fill=\"{}\"/>",
635 colorref_to_css(color)
636 ));
637 }
638 EMR_EXTCREATEFONTINDIRECTW => {
639 if record_size < 104 {
640 return None;
641 }
642 let height = read_i32(data, offset + 12)?;
643 if height >= 0
644 || read_i32(data, offset + 16)? != 0
645 || read_i32(data, offset + 20)? != 0
646 || read_i32(data, offset + 24)? != 0
647 {
648 return None;
649 }
650 let units: Vec<u16> = data[offset + 40..offset + 104]
651 .chunks_exact(2)
652 .map(|v| u16::from_le_bytes([v[0], v[1]]))
653 .take_while(|v| *v != 0)
654 .collect();
655 let font = LogicalFont {
656 family: String::from_utf16(&units).ok()?,
657 height: -(height as f64),
658 weight: read_u32(data, offset + 28)?.clamp(1, 1000),
659 italic: data[offset + 32] != 0,
660 underline: data[offset + 33] != 0,
661 strikeout: data[offset + 34] != 0,
662 };
663 converter
664 .objects
665 .insert(read_u32(data, body)?, DeviceObject::Font(font));
666 }
667 EMR_EXTTEXTOUTW => converter.emit_text(&data[offset..record_end])?,
668 EMR_SETWINDOWEXTEX => {
669 converter.mapping.window_ext = (
670 read_i32(data, body)? as f64,
671 read_i32(data, body + 4)? as f64,
672 );
673 }
674 EMR_SETWINDOWORGEX => {
675 converter.mapping.window_org = (
676 read_i32(data, body)? as f64,
677 read_i32(data, body + 4)? as f64,
678 );
679 }
680 EMR_SETVIEWPORTEXTEX => {
681 converter.mapping.viewport_ext = (
682 read_i32(data, body)? as f64,
683 read_i32(data, body + 4)? as f64,
684 );
685 }
686 EMR_SETVIEWPORTORGEX => {
687 converter.mapping.viewport_org = (
688 read_i32(data, body)? as f64,
689 read_i32(data, body + 4)? as f64,
690 );
691 }
692 EMR_SETPOLYFILLMODE => {
693 converter.fill_rule = if read_u32(data, body)? == 2 {
694 "nonzero"
695 } else {
696 "evenodd"
697 };
698 }
699 EMR_CREATEBRUSHINDIRECT => {
700 let handle = read_u32(data, body)?;
701 let style = read_u32(data, body + 4)?;
702 let color = read_u32(data, body + 8)?;
703 let object = if style == 1 {
704 DeviceObject::NullBrush
705 } else {
706 DeviceObject::SolidBrush(color)
707 };
708 converter.objects.insert(handle, object);
709 }
710 EMR_CREATEPEN => {
711 let handle = read_u32(data, body)?;
712 let style = read_u32(data, body + 4)?;
713 let width = read_i32(data, body + 8)? as f64;
714 let color = read_u32(data, body + 16)?;
715 let object = if style & 0xff == 5 {
716 DeviceObject::NullPen
717 } else {
718 DeviceObject::SolidPen {
719 color,
720 width_logical: width.max(1.0),
721 }
722 };
723 converter.objects.insert(handle, object);
724 }
725 EMR_EXTCREATEPEN => {
726 let handle = read_u32(data, body)?;
727 let pen_style = read_u32(data, body + 20)?;
728 let width = read_u32(data, body + 24)? as f64;
729 let color = read_u32(data, body + 32)?;
730 let object = if pen_style & 0xff == 5 {
731 DeviceObject::NullPen
732 } else {
733 DeviceObject::SolidPen {
734 color,
735 width_logical: width.max(1.0),
736 }
737 };
738 converter.objects.insert(handle, object);
739 }
740 EMR_SELECTOBJECT => {
741 let handle = read_u32(data, body)?;
742 if handle & STOCK_OBJECT_FLAG != 0 {
743 match handle & !STOCK_OBJECT_FLAG {
744 0 => converter.current_brush = Some(0x00ff_ffff),
745 1 => converter.current_brush = Some(0x00c0_c0c0),
746 2 => converter.current_brush = Some(0x0080_8080),
747 3 => converter.current_brush = Some(0x0040_4040),
748 4 => converter.current_brush = Some(0x0000_0000),
749 5 => converter.current_brush = None,
750 6 => converter.current_pen = Some((0x00ff_ffff, 1.0)),
751 7 => converter.current_pen = Some((0x0000_0000, 1.0)),
752 8 => converter.current_pen = None,
753 _ => {}
754 }
755 } else {
756 match converter.objects.get(&handle) {
757 Some(DeviceObject::SolidBrush(color)) => {
758 converter.current_brush = Some(*color);
759 }
760 Some(DeviceObject::NullBrush) => converter.current_brush = None,
761 Some(DeviceObject::SolidPen {
762 color,
763 width_logical,
764 }) => converter.current_pen = Some((*color, *width_logical)),
765 Some(DeviceObject::NullPen) => converter.current_pen = None,
766 Some(DeviceObject::Font(font)) => {
767 converter.current_font = Some(font.clone())
768 }
769 None => {}
770 }
771 }
772 }
773 EMR_DELETEOBJECT => {
774 let handle = read_u32(data, body)?;
775 converter.objects.remove(&handle);
776 }
777 EMR_BEGINPATH => {
778 converter.in_path = true;
779 converter.path_data.clear();
780 }
781 EMR_ENDPATH => {
782 converter.in_path = false;
783 converter.pending_path = Some(std::mem::take(&mut converter.path_data));
784 }
785 EMR_CLOSEFIGURE => {
786 if converter.in_path {
787 converter.path_data.push('Z');
788 }
789 }
790 EMR_MOVETOEX => {
791 let point = converter.map_point(
792 read_i32(data, body)? as f64,
793 read_i32(data, body + 4)? as f64,
794 );
795 converter.current_point = point;
796 if converter.in_path {
797 write!(converter.path_data, "M{} {}", fmt(point.0), fmt(point.1)).expect("writing into a string cannot fail");
798 }
799 }
800 EMR_LINETO => {
801 let start = converter.current_point;
802 let point = converter.map_point(
803 read_i32(data, body)? as f64,
804 read_i32(data, body + 4)? as f64,
805 );
806 converter.current_point = point;
807 if converter.in_path {
808 write!(converter.path_data, "L{} {}", fmt(point.0), fmt(point.1)).expect("writing into a string cannot fail");
809 } else {
810 converter.emit_path(
811 format!(
812 "M{} {}L{} {}",
813 fmt(start.0),
814 fmt(start.1),
815 fmt(point.0),
816 fmt(point.1)
817 ),
818 false,
819 true,
820 );
821 }
822 }
823 EMR_POLYLINETO16 | EMR_POLYBEZIERTO16 => {
824 let points = converter.read_points16(body + 16, record_end)?;
825 if points.is_empty() {
826 offset = record_end;
827 continue;
828 }
829 if !converter.in_path {
830 return None;
831 }
832 if record_type == EMR_POLYLINETO16 {
833 for (x, y) in &points {
834 write!(converter.path_data, "L{} {}", fmt(*x), fmt(*y)).expect("writing into a string cannot fail");
835 }
836 } else {
837 let segment = Converter::bezier_to_d(&points, false)?;
838 converter.path_data.push_str(&segment);
839 }
840 converter.current_point = *points.last().unwrap();
841 }
842 EMR_POLYLINE16 | EMR_POLYGON16 | EMR_POLYBEZIER16 => {
843 let points = converter.read_points16(body + 16, record_end)?;
844 if points.is_empty() {
845 offset = record_end;
846 continue;
847 }
848 let d = if record_type == EMR_POLYBEZIER16 {
849 Converter::bezier_to_d(&points, true)?
850 } else {
851 Converter::polyline_to_d(&points, record_type == EMR_POLYGON16)
852 };
853 if converter.in_path {
854 converter.path_data.push_str(&d);
855 } else if record_type == EMR_POLYGON16 {
856 converter.emit_path(d, true, converter.current_pen.is_some());
857 } else {
858 converter.emit_path(d, false, true);
859 }
860 }
861 EMR_POLYPOLYLINE16 | EMR_POLYPOLYGON16 => {
862 let poly_count = read_u32(data, body + 16)? as usize;
863 let total_points = read_u32(data, body + 20)? as usize;
864 let counts_offset = body + 24;
865 let points_offset = counts_offset + poly_count * 4;
866 let mut d = String::new();
867 let mut cursor = points_offset;
868 let mut consumed = 0usize;
869 for poly_index in 0..poly_count {
870 let count = read_u32(data, counts_offset + poly_index * 4)? as usize;
871 let mut points = Vec::with_capacity(count);
872 for _ in 0..count {
873 if cursor + 4 > record_end {
874 return None;
875 }
876 let x = read_i16(data, cursor)? as f64;
877 let y = read_i16(data, cursor + 2)? as f64;
878 points.push(converter.map_point(x, y));
879 cursor += 4;
880 }
881 consumed += count;
882 d.push_str(&Converter::polyline_to_d(
883 &points,
884 record_type == EMR_POLYPOLYGON16,
885 ));
886 }
887 if consumed != total_points {
888 return None;
889 }
890 if converter.in_path {
891 converter.path_data.push_str(&d);
892 } else if record_type == EMR_POLYPOLYGON16 {
893 converter.emit_path(d, true, converter.current_pen.is_some());
894 } else {
895 converter.emit_path(d, false, true);
896 }
897 }
898 EMR_FILLPATH | EMR_STROKEPATH | EMR_STROKEANDFILLPATH => {
899 let d = converter.pending_path.clone().unwrap_or_default();
900 converter.emit_path(
901 d,
902 record_type != EMR_STROKEPATH,
903 record_type != EMR_FILLPATH,
904 );
905 }
906 EMR_SELECTCLIPPATH => {
907 let Some(d) = converter.pending_path.clone() else {
910 return None;
911 };
912 let clip_id = converter.clip_defs.len();
913 converter.clip_defs.push(format!(
914 "<clipPath id=\"c{clip_id}\" clip-rule=\"{}\"><path d=\"{d}\"/></clipPath>",
915 converter.fill_rule
916 ));
917 converter.active_clip_id = Some(clip_id);
918 }
919 EMR_EXTSELECTCLIPRGN => {
920 let mode = read_u32(data, body + 4)?;
921 let region_data_size = read_u32(data, body)? as usize;
924 if mode == 5 && region_data_size == 0 {
925 converter.active_clip_id = None;
926 } else if mode == 5 {
927 let rect_count = read_u32(data, body + 8 + 8).unwrap_or(0);
928 if rect_count <= 1 {
929 converter.active_clip_id = None;
930 } else {
931 return None;
932 }
933 } else {
934 return None;
935 }
936 }
937 _ => return None,
938 }
939
940 offset = record_end;
941 }
942
943 let (left, top, right, bottom) = converter.bounds;
944 let width = right - left;
945 let height = bottom - top;
946 let defs = if converter.clip_defs.is_empty() {
947 String::new()
948 } else {
949 format!("<defs>{}</defs>", converter.clip_defs.join(""))
950 };
951 Some(format!(
952 "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"{left} {top} {width} {height}\" width=\"{width}\" height=\"{height}\" preserveAspectRatio=\"xMidYMid meet\">{defs}{}</svg>",
953 converter.elements.join("")
954 ))
955}
956
957#[cfg(test)]
958mod coordinate_output_tests {
959 #[test]
960 fn coordinates_preserve_rounding_and_saturated_integer_output() {
961 for (value, expected) in [
962 (0.0, "0"), (-0.0, "0"), (0.004, "0"), (-0.004, "0"),
963 (0.005, "0.01"), (-0.005, "-0.01"), (123.456, "123.46"),
964 (-123.456, "-123.46"), (1234567890123.0, "1234567890123"),
965 (f64::INFINITY, "9223372036854775807"),
966 (f64::NEG_INFINITY, "-9223372036854775808"), (f64::NAN, "NaN"),
967 ] {
968 assert_eq!(super::fmt(value).to_string(), expected);
969 }
970 }
971}
972
973#[cfg(test)]
974mod bounded_coordinate_tests {
975 #[test]
976 fn bounded_decimal_paths_match_standard_float_display() {
977 let mut state = 17u64;
978 for index in 0..1000000 {
979 state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
980 let integer = ((state >> 16) % (1u64 << 41)) as i64 - (1i64 << 40);
981 let value = match index % 4 {
982 0 => integer as f64 / 100.0,
983 1 => f64::from_bits(state),
984 2 => (integer % 10000000) as f64 / 123.0,
985 _ => f64::from_bits((integer as f64 / 100.0).to_bits().wrapping_add(index % 3)),
986 };
987 let rounded = (value * 100.0).round() / 100.0;
988 let expected = if rounded == rounded.trunc() {
989 format!("{}", rounded as i64)
990 } else {
991 format!("{rounded}")
992 };
993 assert_eq!(super::fmt(value).to_string(), expected);
994 }
995 }
996}
997
998#[cfg(test)]
999mod positioned_text_tests {
1000 use super::*;
1001
1002 fn render(advances: &[(i32, i32)], align: u32) -> String {
1003 let units: Vec<u16> = "A😀B".encode_utf16().collect();
1004 let text_end = 76 + units.len() * 2;
1005 let mut record = vec![0u8; text_end + advances.len() * 8];
1006 for (offset, value) in [
1007 (36, 100u32), (40, 200), (44, units.len() as u32),
1008 (48, 76), (52, if advances.is_empty() { 0 } else { 0x2000 }),
1009 (72, if advances.is_empty() { 0 } else { text_end as u32 }),
1010 ] {
1011 record[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1012 }
1013 for (index, unit) in units.iter().enumerate() {
1014 record[76 + index * 2..78 + index * 2].copy_from_slice(&unit.to_le_bytes());
1015 }
1016 for (index, (x, y)) in advances.iter().enumerate() {
1017 let offset = text_end + index * 8;
1018 record[offset..offset + 4].copy_from_slice(&x.to_le_bytes());
1019 record[offset + 4..offset + 8].copy_from_slice(&y.to_le_bytes());
1020 }
1021 let mut converter = Converter {
1022 data: &[],
1023 mapping: Mapping { window_org: (0.0, 0.0), window_ext: (2.0, 4.0),
1024 viewport_org: (0.0, 0.0), viewport_ext: (3.0, -2.0) },
1025 objects: std::collections::HashMap::new(), current_brush: None, current_pen: None,
1026 current_font: Some(LogicalFont { family: "serif".into(), height: 10.0, weight: 400,
1027 italic: false, underline: false, strikeout: false }),
1028 text_color: 0, background_color: 0xffffff, background_mode: 1, text_align: align,
1029 saved_states: Vec::new(), fill_rule: "nonzero", in_path: false,
1030 path_data: String::new(), pending_path: None, current_point: (0.0, 0.0),
1031 elements: Vec::new(), clip_defs: Vec::new(), active_clip_id: None, bounds: (0, 0, 1000, 1000),
1032 };
1033 assert_eq!(converter.emit_text(&record), Some(()));
1034 assert_eq!(converter.elements.len(), 1);
1035 converter.elements.pop().unwrap()
1036 }
1037
1038 #[test]
1039 fn paired_advances_preserve_surrogate_positions_and_alignment() {
1040 assert_eq!(
1041 render(&[(4, 8), (5, -4), (3, 4), (-2, 0)], 6 | 24),
1042 "<text xml:space=\"preserve\" x=\"142.5 148.5 160.5\" y=\"-100 -104 -104\" font-family=\"serif\" font-size=\"5\" font-weight=\"400\" font-style=\"normal\" text-decoration=\"\" text-anchor=\"start\" dominant-baseline=\"alphabetic\" fill=\"#000000\">A😀B</text>"
1043 );
1044 }
1045
1046 #[test]
1047 fn unpositioned_text_retains_one_anchor_point() {
1048 assert_eq!(
1049 render(&[], 2 | 24),
1050 "<text xml:space=\"preserve\" x=\"150\" y=\"-100\" font-family=\"serif\" font-size=\"5\" font-weight=\"400\" font-style=\"normal\" text-decoration=\"\" text-anchor=\"end\" dominant-baseline=\"alphabetic\" fill=\"#000000\">A😀B</text>"
1051 );
1052 }
1053}