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