Skip to main content

rust_docx/
emf.rs

1//! EMF (Enhanced Metafile) to SVG conversion for the supported drawing
2//! record subset: window/viewport mapping, paths, solid brushes/pens,
3//! positioned Unicode text, solid raster-operation fills, saved device
4//! contexts, fill modes, clipping, and opaque bitmap transfers. Records outside the
5//! supported set abort the conversion so the caller can fall back to the
6//! unsupported-image placeholder instead of rendering something wrong.
7
8use 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            // BI_RGB's fourth byte is reserved; SRCCOPY is opaque.
64            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('&', "&amp;")
106        .replace('<', "&lt;")
107        .replace('>', "&gt;")
108        .replace('"', "&quot;")
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            // Scaled integers are exact here, and distinct cents map to distinct f64 values.
204            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
648/// Converts an EMF byte stream to an SVG document when every record falls in
649/// the supported drawing subset; returns `None` otherwise so callers keep the
650/// placeholder fallback.
651pub 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                // All combine modes are approximated as replace; logos use
1042                // RGN_COPY in practice.
1043                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                // RGN_COPY with no region data resets the clip; anything more
1056                // elaborate than a reset or a full-bounds region bails.
1057                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}