use std::fmt::Write as _;
fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
data.get(offset..offset + 4)
.map(|bytes| u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn read_i32(data: &[u8], offset: usize) -> Option<i32> {
read_u32(data, offset).map(|value| value as i32)
}
fn read_i16(data: &[u8], offset: usize) -> Option<i16> {
data.get(offset..offset + 2)
.map(|bytes| i16::from_le_bytes([bytes[0], bytes[1]]))
}
fn escape_svg(value: &str) -> String {
value
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn colorref_to_css(color: u32) -> String {
let red = color & 0xff;
let green = (color >> 8) & 0xff;
let blue = (color >> 16) & 0xff;
format!("#{red:02x}{green:02x}{blue:02x}")
}
#[derive(Clone, Debug, PartialEq)]
enum DeviceObject {
SolidBrush(u32),
NullBrush,
SolidPen { color: u32, width_logical: f64 },
NullPen,
Font(LogicalFont),
}
#[derive(Clone, Debug, PartialEq)]
struct LogicalFont {
family: String,
height: f64,
weight: u32,
italic: bool,
underline: bool,
strikeout: bool,
}
#[derive(Clone)]
struct SavedState {
mapping: Mapping,
brush: Option<u32>,
pen: Option<(u32, f64)>,
font: Option<LogicalFont>,
text_color: u32,
background_color: u32,
background_mode: u32,
text_align: u32,
fill_rule: &'static str,
current_point: (f64, f64),
clip: Option<usize>,
}
#[derive(Clone, Copy, Debug)]
struct Mapping {
window_org: (f64, f64),
window_ext: (f64, f64),
viewport_org: (f64, f64),
viewport_ext: (f64, f64),
}
impl Mapping {
fn identity() -> Self {
Mapping {
window_org: (0.0, 0.0),
window_ext: (1.0, 1.0),
viewport_org: (0.0, 0.0),
viewport_ext: (1.0, 1.0),
}
}
fn scale_x(&self) -> f64 {
if self.window_ext.0 == 0.0 {
1.0
} else {
self.viewport_ext.0 / self.window_ext.0
}
}
fn scale_y(&self) -> f64 {
if self.window_ext.1 == 0.0 {
1.0
} else {
self.viewport_ext.1 / self.window_ext.1
}
}
fn map(&self, x: f64, y: f64) -> (f64, f64) {
(
(x - self.window_org.0) * self.scale_x() + self.viewport_org.0,
(y - self.window_org.1) * self.scale_y() + self.viewport_org.1,
)
}
}
struct Coordinate(f64);
impl std::fmt::Display for Coordinate {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let scaled = (self.0 * 100.0).round();
let rounded = scaled / 100.0;
if rounded == rounded.trunc() {
write!(formatter, "{}", rounded as i64)
} else if scaled.abs() <= (1u64 << 40) as f64 {
let value = scaled as i64;
let absolute = value.unsigned_abs();
if value < 0 {
formatter.write_str("-")?;
}
write!(formatter, "{}.", absolute / 100)?;
let fraction = absolute % 100;
if fraction < 10 {
write!(formatter, "0{fraction}")
} else if fraction % 10 == 0 {
write!(formatter, "{}", fraction / 10)
} else {
write!(formatter, "{fraction}")
}
} else {
write!(formatter, "{rounded}")
}
}
}
fn fmt(value: f64) -> Coordinate {
Coordinate(value)
}
const EMR_HEADER: u32 = 1;
const EMR_POLYGON16: u32 = 86;
const EMR_POLYLINE16: u32 = 87;
const EMR_POLYBEZIER16: u32 = 85;
const EMR_POLYBEZIERTO16: u32 = 88;
const EMR_POLYLINETO16: u32 = 89;
const EMR_POLYPOLYLINE16: u32 = 90;
const EMR_POLYPOLYGON16: u32 = 91;
const EMR_SETWINDOWEXTEX: u32 = 9;
const EMR_SETWINDOWORGEX: u32 = 10;
const EMR_SETVIEWPORTEXTEX: u32 = 11;
const EMR_SETVIEWPORTORGEX: u32 = 12;
const EMR_SETBRUSHORGEX: u32 = 13;
const EMR_EOF: u32 = 14;
const EMR_SETMAPMODE: u32 = 17;
const EMR_SETBKMODE: u32 = 18;
const EMR_SETPOLYFILLMODE: u32 = 19;
const EMR_SETROP2: u32 = 20;
const EMR_SETSTRETCHBLTMODE: u32 = 21;
const EMR_SETTEXTALIGN: u32 = 22;
const EMR_SETTEXTCOLOR: u32 = 24;
const EMR_SETBKCOLOR: u32 = 25;
const EMR_MOVETOEX: u32 = 27;
const EMR_SETMETARGN: u32 = 28;
const EMR_SETMITERLIMIT: u32 = 58;
const EMR_LINETO: u32 = 54;
const EMR_SELECTOBJECT: u32 = 37;
const EMR_CREATEPEN: u32 = 38;
const EMR_CREATEBRUSHINDIRECT: u32 = 39;
const EMR_DELETEOBJECT: u32 = 40;
const EMR_BEGINPATH: u32 = 59;
const EMR_ENDPATH: u32 = 60;
const EMR_CLOSEFIGURE: u32 = 61;
const EMR_FILLPATH: u32 = 62;
const EMR_STROKEANDFILLPATH: u32 = 63;
const EMR_STROKEPATH: u32 = 64;
const EMR_SELECTCLIPPATH: u32 = 67;
const EMR_COMMENT: u32 = 70;
const EMR_SAVEDC: u32 = 33;
const EMR_RESTOREDC: u32 = 34;
const EMR_INTERSECTCLIPRECT: u32 = 30;
const EMR_BITBLT: u32 = 76;
const EMR_EXTCREATEFONTINDIRECTW: u32 = 82;
const EMR_EXTTEXTOUTW: u32 = 84;
const EMR_EXTSELECTCLIPRGN: u32 = 75;
const EMR_EXTCREATEPEN: u32 = 95;
const EMR_SETICMMODE: u32 = 98;
const STOCK_OBJECT_FLAG: u32 = 0x8000_0000;
struct Converter<'a> {
data: &'a [u8],
mapping: Mapping,
objects: std::collections::HashMap<u32, DeviceObject>,
current_brush: Option<u32>,
current_pen: Option<(u32, f64)>,
current_font: Option<LogicalFont>,
text_color: u32,
background_color: u32,
background_mode: u32,
text_align: u32,
saved_states: Vec<SavedState>,
fill_rule: &'static str,
in_path: bool,
path_data: String,
pending_path: Option<String>,
current_point: (f64, f64),
elements: Vec<String>,
clip_defs: Vec<String>,
active_clip_id: Option<usize>,
bounds: (i32, i32, i32, i32),
}
impl<'a> Converter<'a> {
fn save_state(&self) -> SavedState {
SavedState {
mapping: self.mapping,
brush: self.current_brush,
pen: self.current_pen,
font: self.current_font.clone(),
text_color: self.text_color,
background_color: self.background_color,
background_mode: self.background_mode,
text_align: self.text_align,
fill_rule: self.fill_rule,
current_point: self.current_point,
clip: self.active_clip_id,
}
}
fn restore_state(&mut self, state: SavedState) {
self.mapping = state.mapping;
self.current_brush = state.brush;
self.current_pen = state.pen;
self.current_font = state.font;
self.text_color = state.text_color;
self.background_color = state.background_color;
self.background_mode = state.background_mode;
self.text_align = state.text_align;
self.fill_rule = state.fill_rule;
self.current_point = state.current_point;
self.active_clip_id = state.clip;
}
fn mapped_rect(&self, x: f64, y: f64, width: f64, height: f64) -> String {
let first = self.map_point(x, y);
let last = self.map_point(x + width, y + height);
format!(
"x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"",
fmt(first.0.min(last.0)),
fmt(first.1.min(last.1)),
fmt((last.0 - first.0).abs()),
fmt((last.1 - first.1).abs())
)
}
fn intersect_clip_rect(&mut self, rect: String) {
let id = self.clip_defs.len();
let previous = self
.active_clip_id
.map(|id| format!(" clip-path=\"url(#c{id})\""))
.unwrap_or_default();
self.clip_defs.push(format!(
"<clipPath id=\"c{id}\"><rect {rect}{previous}/></clipPath>"
));
self.active_clip_id = Some(id);
}
fn emit_text(&mut self, record: &[u8]) -> Option<()> {
if record.len() < 76 || self.in_path {
return None;
}
let font = self.current_font.clone()?;
let count = read_u32(record, 44)? as usize;
let string_offset = read_u32(record, 48)? as usize;
let options = read_u32(record, 52)?;
let dx_offset = read_u32(record, 72)? as usize;
if count > 0 && string_offset < 76 {
return None;
}
if options & !(2 | 4 | 0x1000 | 0x2000) != 0 || self.text_align & 1 != 0 {
return None;
}
let encoded =
record.get(string_offset..string_offset.checked_add(count.checked_mul(2)?)?)?;
let units: Vec<u16> = encoded
.chunks_exact(2)
.map(|v| u16::from_le_bytes([v[0], v[1]]))
.collect();
let text = String::from_utf16(&units).ok()?;
if text.is_empty() {
return Some(());
}
let x = read_i32(record, 36)? as f64;
let y = read_i32(record, 40)? as f64;
let mut advances = Vec::new();
if dx_offset != 0 {
let stride = if options & 0x2000 != 0 { 8 } else { 4 };
if dx_offset < 76 || dx_offset.checked_add(count.checked_mul(stride)?)? > record.len() {
return None;
}
for index in 0..count {
let offset = dx_offset + index * stride;
advances.push((
read_i32(record, offset)? as f64,
if stride == 8 {
read_i32(record, offset + 4)? as f64
} else {
0.0
},
));
}
}
let old_clip = self.active_clip_id;
let rectangle = self.mapped_rect(
read_i32(record, 56)? as f64,
read_i32(record, 60)? as f64,
(read_i32(record, 64)? as f64) - (read_i32(record, 56)? as f64),
(read_i32(record, 68)? as f64) - (read_i32(record, 60)? as f64),
);
if options & 4 != 0 {
self.intersect_clip_rect(rectangle.clone());
}
if options & 2 != 0 {
self.push_element(format!(
"<rect {rectangle} fill=\"{}\"/>",
colorref_to_css(self.background_color)
));
} else if self.background_mode == 2 {
let rect = self.mapped_rect(
read_i32(record, 8)? as f64,
read_i32(record, 12)? as f64,
(read_i32(record, 16)? as f64) - (read_i32(record, 8)? as f64) + 1.0,
(read_i32(record, 20)? as f64) - (read_i32(record, 12)? as f64) + 1.0,
);
self.push_element(format!(
"<rect {rect} fill=\"{}\"/>",
colorref_to_css(self.background_color)
));
}
let horizontal = self.text_align & 6;
let width: f64 = advances.iter().map(|v| v.0).sum();
let shift = if horizontal == 6 {
width / 2.0
} else if horizontal == 2 {
width
} else {
0.0
};
let coordinate_capacity = if advances.is_empty() { 16 } else { count.saturating_mul(6) };
let mut xs = String::with_capacity(coordinate_capacity);
let mut ys = String::with_capacity(coordinate_capacity);
let mut position = (x - shift, y);
let mut unit_index = 0;
if advances.is_empty() {
let point = self.map_point(x, y);
let _ = write!(xs, "{}", fmt(point.0));
let _ = write!(ys, "{}", fmt(point.1));
} else {
for character in text.chars() {
let point = self.map_point(position.0, position.1);
if !xs.is_empty() {
xs.push(' ');
ys.push(' ');
}
let _ = write!(xs, "{}", fmt(point.0));
let _ = write!(ys, "{}", fmt(point.1));
for _ in 0..character.len_utf16() {
position.0 += advances[unit_index].0;
position.1 += advances[unit_index].1;
unit_index += 1;
}
}
}
let anchor = if advances.is_empty() && horizontal == 6 {
"middle"
} else if advances.is_empty() && horizontal == 2 {
"end"
} else {
"start"
};
let baseline = match self.text_align & 24 {
24 => "alphabetic",
8 => "text-after-edge",
_ => "text-before-edge",
};
let decoration = [
if font.underline { "underline" } else { "" },
if font.strikeout { "line-through" } else { "" },
]
.join(" ");
self.push_element(format!(
"<text xml:space=\"preserve\" x=\"{}\" y=\"{}\" font-family=\"{}\" font-size=\"{}\" font-weight=\"{}\" font-style=\"{}\" text-decoration=\"{}\" text-anchor=\"{anchor}\" dominant-baseline=\"{baseline}\" fill=\"{}\">{}</text>",
xs,ys,escape_svg(&font.family),fmt(font.height*self.mapping.scale_y().abs()),font.weight,
if font.italic { "italic" } else { "normal" },decoration.trim(),colorref_to_css(self.text_color),escape_svg(&text)));
self.active_clip_id = old_clip;
Some(())
}
fn map_point(&self, x: f64, y: f64) -> (f64, f64) {
self.mapping.map(x, y)
}
fn push_element(&mut self, element: String) {
if let Some(clip_id) = self.active_clip_id {
self.elements
.push(format!("<g clip-path=\"url(#c{clip_id})\">{element}</g>"));
} else {
self.elements.push(element);
}
}
fn fill_attr(&self) -> String {
match self.current_brush {
Some(color) => format!("fill=\"{}\"", colorref_to_css(color)),
None => "fill=\"none\"".to_string(),
}
}
fn stroke_attr(&self) -> String {
match self.current_pen {
Some((color, width_logical)) => {
let width_px = (width_logical * self.mapping.scale_x()).abs().max(0.25);
format!(
"stroke=\"{}\" stroke-width=\"{}\"",
colorref_to_css(color),
fmt(width_px)
)
}
None => "stroke=\"none\"".to_string(),
}
}
fn emit_path(&mut self, d: String, fill: bool, stroke: bool) {
if d.is_empty() {
return;
}
let fill_attr = if fill {
format!("{} fill-rule=\"{}\"", self.fill_attr(), self.fill_rule)
} else {
"fill=\"none\"".to_string()
};
let stroke_attr = if stroke {
self.stroke_attr()
} else {
"stroke=\"none\"".to_string()
};
self.push_element(format!("<path d=\"{d}\" {fill_attr} {stroke_attr}/>"));
}
fn read_points16(&self, offset: usize, record_end: usize) -> Option<Vec<(f64, f64)>> {
let count = read_u32(self.data, offset)? as usize;
let mut points = Vec::with_capacity(count);
let mut cursor = offset + 4;
for _ in 0..count {
if cursor + 4 > record_end {
return None;
}
let x = read_i16(self.data, cursor)? as f64;
let y = read_i16(self.data, cursor + 2)? as f64;
points.push(self.map_point(x, y));
cursor += 4;
}
Some(points)
}
fn polyline_to_d(points: &[(f64, f64)], close: bool) -> String {
let mut d = String::new();
for (index, (x, y)) in points.iter().enumerate() {
if index == 0 {
write!(d, "M{} {}", fmt(*x), fmt(*y)).expect("writing into a string cannot fail");
} else {
write!(d, "L{} {}", fmt(*x), fmt(*y)).expect("writing into a string cannot fail");
}
}
if close && !d.is_empty() {
d.push('Z');
}
d
}
fn bezier_to_d(points: &[(f64, f64)], starts_with_move: bool) -> Option<String> {
let mut d = String::new();
let control_points = if starts_with_move {
let Some(((x, y), rest)) = points.split_first() else {
return None;
};
write!(d, "M{} {}", fmt(*x), fmt(*y)).expect("writing into a string cannot fail");
rest
} else {
points
};
if control_points.len() % 3 != 0 {
return None;
}
for triple in control_points.chunks(3) {
write!(d,
"C{} {} {} {} {} {}",
fmt(triple[0].0),
fmt(triple[0].1),
fmt(triple[1].0),
fmt(triple[1].1),
fmt(triple[2].0),
fmt(triple[2].1)
).expect("writing into a string cannot fail");
}
Some(d)
}
}
pub fn emf_to_svg(data: &[u8]) -> Option<String> {
if data.len() < 88 || read_u32(data, 0)? != EMR_HEADER {
return None;
}
let bounds = (
read_i32(data, 8)?,
read_i32(data, 12)?,
read_i32(data, 16)?,
read_i32(data, 20)?,
);
if bounds.2 <= bounds.0 || bounds.3 <= bounds.1 {
return None;
}
let mut converter = Converter {
data,
mapping: Mapping::identity(),
objects: std::collections::HashMap::new(),
current_brush: Some(0x00ff_ffff),
current_pen: Some((0, 1.0)),
current_font: None,
text_color: 0,
background_color: 0x00ff_ffff,
background_mode: 2,
text_align: 0,
saved_states: Vec::new(),
fill_rule: "evenodd",
in_path: false,
path_data: String::new(),
pending_path: None,
current_point: (0.0, 0.0),
elements: Vec::new(),
clip_defs: Vec::new(),
active_clip_id: None,
bounds,
};
let header_size = read_u32(data, 4)? as usize;
let mut offset = header_size;
while offset + 8 <= data.len() {
let record_type = read_u32(data, offset)?;
let record_size = read_u32(data, offset + 4)? as usize;
if record_size < 8 || offset + record_size > data.len() {
return None;
}
let record_end = offset + record_size;
let body = offset + 8;
match record_type {
EMR_EOF => break,
EMR_HEADER => return None,
EMR_SETMAPMODE
| EMR_SETSTRETCHBLTMODE
| EMR_SETBRUSHORGEX
| EMR_SETMETARGN
| EMR_SETMITERLIMIT
| EMR_SETICMMODE
| EMR_COMMENT => {}
EMR_SAVEDC => converter.saved_states.push(converter.save_state()),
EMR_RESTOREDC => {
let level = read_i32(data, body)?;
let index = if level < 0 {
converter
.saved_states
.len()
.checked_sub(level.unsigned_abs() as usize)?
} else {
(level as usize).checked_sub(1)?
};
let state = converter.saved_states.get(index)?.clone();
converter.saved_states.truncate(index);
converter.restore_state(state);
}
EMR_SETROP2 => {
if read_u32(data, body)? != 13 {
return None;
}
}
EMR_SETBKMODE => converter.background_mode = read_u32(data, body)?,
EMR_SETTEXTALIGN => converter.text_align = read_u32(data, body)?,
EMR_SETTEXTCOLOR => converter.text_color = read_u32(data, body)?,
EMR_SETBKCOLOR => converter.background_color = read_u32(data, body)?,
EMR_INTERSECTCLIPRECT => {
if record_size < 24 {
return None;
}
let left = read_i32(data, body)? as f64;
let top = read_i32(data, body + 4)? as f64;
let rect = converter.mapped_rect(
left,
top,
read_i32(data, body + 8)? as f64 - left,
read_i32(data, body + 12)? as f64 - top,
);
converter.intersect_clip_rect(rect);
}
EMR_BITBLT => {
if record_size < 100 {
return None;
}
let color = match read_u32(data, offset + 40)? {
0x00f00021 => converter.current_brush?,
0x00000042 => 0,
0x00ff0062 => 0x00ffffff,
_ => return None,
};
let rect = converter.mapped_rect(
read_i32(data, offset + 24)? as f64,
read_i32(data, offset + 28)? as f64,
read_i32(data, offset + 32)? as f64,
read_i32(data, offset + 36)? as f64,
);
converter.push_element(format!(
"<rect {rect} fill=\"{}\"/>",
colorref_to_css(color)
));
}
EMR_EXTCREATEFONTINDIRECTW => {
if record_size < 104 {
return None;
}
let height = read_i32(data, offset + 12)?;
if height >= 0
|| read_i32(data, offset + 16)? != 0
|| read_i32(data, offset + 20)? != 0
|| read_i32(data, offset + 24)? != 0
{
return None;
}
let units: Vec<u16> = data[offset + 40..offset + 104]
.chunks_exact(2)
.map(|v| u16::from_le_bytes([v[0], v[1]]))
.take_while(|v| *v != 0)
.collect();
let font = LogicalFont {
family: String::from_utf16(&units).ok()?,
height: -(height as f64),
weight: read_u32(data, offset + 28)?.clamp(1, 1000),
italic: data[offset + 32] != 0,
underline: data[offset + 33] != 0,
strikeout: data[offset + 34] != 0,
};
converter
.objects
.insert(read_u32(data, body)?, DeviceObject::Font(font));
}
EMR_EXTTEXTOUTW => converter.emit_text(&data[offset..record_end])?,
EMR_SETWINDOWEXTEX => {
converter.mapping.window_ext = (
read_i32(data, body)? as f64,
read_i32(data, body + 4)? as f64,
);
}
EMR_SETWINDOWORGEX => {
converter.mapping.window_org = (
read_i32(data, body)? as f64,
read_i32(data, body + 4)? as f64,
);
}
EMR_SETVIEWPORTEXTEX => {
converter.mapping.viewport_ext = (
read_i32(data, body)? as f64,
read_i32(data, body + 4)? as f64,
);
}
EMR_SETVIEWPORTORGEX => {
converter.mapping.viewport_org = (
read_i32(data, body)? as f64,
read_i32(data, body + 4)? as f64,
);
}
EMR_SETPOLYFILLMODE => {
converter.fill_rule = if read_u32(data, body)? == 2 {
"nonzero"
} else {
"evenodd"
};
}
EMR_CREATEBRUSHINDIRECT => {
let handle = read_u32(data, body)?;
let style = read_u32(data, body + 4)?;
let color = read_u32(data, body + 8)?;
let object = if style == 1 {
DeviceObject::NullBrush
} else {
DeviceObject::SolidBrush(color)
};
converter.objects.insert(handle, object);
}
EMR_CREATEPEN => {
let handle = read_u32(data, body)?;
let style = read_u32(data, body + 4)?;
let width = read_i32(data, body + 8)? as f64;
let color = read_u32(data, body + 16)?;
let object = if style & 0xff == 5 {
DeviceObject::NullPen
} else {
DeviceObject::SolidPen {
color,
width_logical: width.max(1.0),
}
};
converter.objects.insert(handle, object);
}
EMR_EXTCREATEPEN => {
let handle = read_u32(data, body)?;
let pen_style = read_u32(data, body + 20)?;
let width = read_u32(data, body + 24)? as f64;
let color = read_u32(data, body + 32)?;
let object = if pen_style & 0xff == 5 {
DeviceObject::NullPen
} else {
DeviceObject::SolidPen {
color,
width_logical: width.max(1.0),
}
};
converter.objects.insert(handle, object);
}
EMR_SELECTOBJECT => {
let handle = read_u32(data, body)?;
if handle & STOCK_OBJECT_FLAG != 0 {
match handle & !STOCK_OBJECT_FLAG {
0 => converter.current_brush = Some(0x00ff_ffff),
1 => converter.current_brush = Some(0x00c0_c0c0),
2 => converter.current_brush = Some(0x0080_8080),
3 => converter.current_brush = Some(0x0040_4040),
4 => converter.current_brush = Some(0x0000_0000),
5 => converter.current_brush = None,
6 => converter.current_pen = Some((0x00ff_ffff, 1.0)),
7 => converter.current_pen = Some((0x0000_0000, 1.0)),
8 => converter.current_pen = None,
_ => {}
}
} else {
match converter.objects.get(&handle) {
Some(DeviceObject::SolidBrush(color)) => {
converter.current_brush = Some(*color);
}
Some(DeviceObject::NullBrush) => converter.current_brush = None,
Some(DeviceObject::SolidPen {
color,
width_logical,
}) => converter.current_pen = Some((*color, *width_logical)),
Some(DeviceObject::NullPen) => converter.current_pen = None,
Some(DeviceObject::Font(font)) => {
converter.current_font = Some(font.clone())
}
None => {}
}
}
}
EMR_DELETEOBJECT => {
let handle = read_u32(data, body)?;
converter.objects.remove(&handle);
}
EMR_BEGINPATH => {
converter.in_path = true;
converter.path_data.clear();
}
EMR_ENDPATH => {
converter.in_path = false;
converter.pending_path = Some(std::mem::take(&mut converter.path_data));
}
EMR_CLOSEFIGURE => {
if converter.in_path {
converter.path_data.push('Z');
}
}
EMR_MOVETOEX => {
let point = converter.map_point(
read_i32(data, body)? as f64,
read_i32(data, body + 4)? as f64,
);
converter.current_point = point;
if converter.in_path {
write!(converter.path_data, "M{} {}", fmt(point.0), fmt(point.1)).expect("writing into a string cannot fail");
}
}
EMR_LINETO => {
let start = converter.current_point;
let point = converter.map_point(
read_i32(data, body)? as f64,
read_i32(data, body + 4)? as f64,
);
converter.current_point = point;
if converter.in_path {
write!(converter.path_data, "L{} {}", fmt(point.0), fmt(point.1)).expect("writing into a string cannot fail");
} else {
converter.emit_path(
format!(
"M{} {}L{} {}",
fmt(start.0),
fmt(start.1),
fmt(point.0),
fmt(point.1)
),
false,
true,
);
}
}
EMR_POLYLINETO16 | EMR_POLYBEZIERTO16 => {
let points = converter.read_points16(body + 16, record_end)?;
if points.is_empty() {
offset = record_end;
continue;
}
if !converter.in_path {
return None;
}
if record_type == EMR_POLYLINETO16 {
for (x, y) in &points {
write!(converter.path_data, "L{} {}", fmt(*x), fmt(*y)).expect("writing into a string cannot fail");
}
} else {
let segment = Converter::bezier_to_d(&points, false)?;
converter.path_data.push_str(&segment);
}
converter.current_point = *points.last().unwrap();
}
EMR_POLYLINE16 | EMR_POLYGON16 | EMR_POLYBEZIER16 => {
let points = converter.read_points16(body + 16, record_end)?;
if points.is_empty() {
offset = record_end;
continue;
}
let d = if record_type == EMR_POLYBEZIER16 {
Converter::bezier_to_d(&points, true)?
} else {
Converter::polyline_to_d(&points, record_type == EMR_POLYGON16)
};
if converter.in_path {
converter.path_data.push_str(&d);
} else if record_type == EMR_POLYGON16 {
converter.emit_path(d, true, converter.current_pen.is_some());
} else {
converter.emit_path(d, false, true);
}
}
EMR_POLYPOLYLINE16 | EMR_POLYPOLYGON16 => {
let poly_count = read_u32(data, body + 16)? as usize;
let total_points = read_u32(data, body + 20)? as usize;
let counts_offset = body + 24;
let points_offset = counts_offset + poly_count * 4;
let mut d = String::new();
let mut cursor = points_offset;
let mut consumed = 0usize;
for poly_index in 0..poly_count {
let count = read_u32(data, counts_offset + poly_index * 4)? as usize;
let mut points = Vec::with_capacity(count);
for _ in 0..count {
if cursor + 4 > record_end {
return None;
}
let x = read_i16(data, cursor)? as f64;
let y = read_i16(data, cursor + 2)? as f64;
points.push(converter.map_point(x, y));
cursor += 4;
}
consumed += count;
d.push_str(&Converter::polyline_to_d(
&points,
record_type == EMR_POLYPOLYGON16,
));
}
if consumed != total_points {
return None;
}
if converter.in_path {
converter.path_data.push_str(&d);
} else if record_type == EMR_POLYPOLYGON16 {
converter.emit_path(d, true, converter.current_pen.is_some());
} else {
converter.emit_path(d, false, true);
}
}
EMR_FILLPATH | EMR_STROKEPATH | EMR_STROKEANDFILLPATH => {
let d = converter.pending_path.clone().unwrap_or_default();
converter.emit_path(
d,
record_type != EMR_STROKEPATH,
record_type != EMR_FILLPATH,
);
}
EMR_SELECTCLIPPATH => {
let Some(d) = converter.pending_path.clone() else {
return None;
};
let clip_id = converter.clip_defs.len();
converter.clip_defs.push(format!(
"<clipPath id=\"c{clip_id}\" clip-rule=\"{}\"><path d=\"{d}\"/></clipPath>",
converter.fill_rule
));
converter.active_clip_id = Some(clip_id);
}
EMR_EXTSELECTCLIPRGN => {
let mode = read_u32(data, body + 4)?;
let region_data_size = read_u32(data, body)? as usize;
if mode == 5 && region_data_size == 0 {
converter.active_clip_id = None;
} else if mode == 5 {
let rect_count = read_u32(data, body + 8 + 8).unwrap_or(0);
if rect_count <= 1 {
converter.active_clip_id = None;
} else {
return None;
}
} else {
return None;
}
}
_ => return None,
}
offset = record_end;
}
let (left, top, right, bottom) = converter.bounds;
let width = right - left;
let height = bottom - top;
let defs = if converter.clip_defs.is_empty() {
String::new()
} else {
format!("<defs>{}</defs>", converter.clip_defs.join(""))
};
Some(format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"{left} {top} {width} {height}\" width=\"{width}\" height=\"{height}\" preserveAspectRatio=\"xMidYMid meet\">{defs}{}</svg>",
converter.elements.join("")
))
}
#[cfg(test)]
mod coordinate_output_tests {
#[test]
fn coordinates_preserve_rounding_and_saturated_integer_output() {
for (value, expected) in [
(0.0, "0"), (-0.0, "0"), (0.004, "0"), (-0.004, "0"),
(0.005, "0.01"), (-0.005, "-0.01"), (123.456, "123.46"),
(-123.456, "-123.46"), (1234567890123.0, "1234567890123"),
(f64::INFINITY, "9223372036854775807"),
(f64::NEG_INFINITY, "-9223372036854775808"), (f64::NAN, "NaN"),
] {
assert_eq!(super::fmt(value).to_string(), expected);
}
}
}
#[cfg(test)]
mod bounded_coordinate_tests {
#[test]
fn bounded_decimal_paths_match_standard_float_display() {
let mut state = 17u64;
for index in 0..1000000 {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
let integer = ((state >> 16) % (1u64 << 41)) as i64 - (1i64 << 40);
let value = match index % 4 {
0 => integer as f64 / 100.0,
1 => f64::from_bits(state),
2 => (integer % 10000000) as f64 / 123.0,
_ => f64::from_bits((integer as f64 / 100.0).to_bits().wrapping_add(index % 3)),
};
let rounded = (value * 100.0).round() / 100.0;
let expected = if rounded == rounded.trunc() {
format!("{}", rounded as i64)
} else {
format!("{rounded}")
};
assert_eq!(super::fmt(value).to_string(), expected);
}
}
}
#[cfg(test)]
mod positioned_text_tests {
use super::*;
fn render(advances: &[(i32, i32)], align: u32) -> String {
let units: Vec<u16> = "A😀B".encode_utf16().collect();
let text_end = 76 + units.len() * 2;
let mut record = vec![0u8; text_end + advances.len() * 8];
for (offset, value) in [
(36, 100u32), (40, 200), (44, units.len() as u32),
(48, 76), (52, if advances.is_empty() { 0 } else { 0x2000 }),
(72, if advances.is_empty() { 0 } else { text_end as u32 }),
] {
record[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
for (index, unit) in units.iter().enumerate() {
record[76 + index * 2..78 + index * 2].copy_from_slice(&unit.to_le_bytes());
}
for (index, (x, y)) in advances.iter().enumerate() {
let offset = text_end + index * 8;
record[offset..offset + 4].copy_from_slice(&x.to_le_bytes());
record[offset + 4..offset + 8].copy_from_slice(&y.to_le_bytes());
}
let mut converter = Converter {
data: &[],
mapping: Mapping { window_org: (0.0, 0.0), window_ext: (2.0, 4.0),
viewport_org: (0.0, 0.0), viewport_ext: (3.0, -2.0) },
objects: std::collections::HashMap::new(), current_brush: None, current_pen: None,
current_font: Some(LogicalFont { family: "serif".into(), height: 10.0, weight: 400,
italic: false, underline: false, strikeout: false }),
text_color: 0, background_color: 0xffffff, background_mode: 1, text_align: align,
saved_states: Vec::new(), fill_rule: "nonzero", in_path: false,
path_data: String::new(), pending_path: None, current_point: (0.0, 0.0),
elements: Vec::new(), clip_defs: Vec::new(), active_clip_id: None, bounds: (0, 0, 1000, 1000),
};
assert_eq!(converter.emit_text(&record), Some(()));
assert_eq!(converter.elements.len(), 1);
converter.elements.pop().unwrap()
}
#[test]
fn paired_advances_preserve_surrogate_positions_and_alignment() {
assert_eq!(
render(&[(4, 8), (5, -4), (3, 4), (-2, 0)], 6 | 24),
"<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>"
);
}
#[test]
fn unpositioned_text_retains_one_anchor_point() {
assert_eq!(
render(&[], 2 | 24),
"<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>"
);
}
}