#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
pub enum PenTip {
#[default]
Ball,
Flat,
}
impl PenTip {
pub fn to_xml(&self) -> &'static str {
match self {
PenTip::Ball => "ball",
PenTip::Flat => "flat",
}
}
}
#[derive(Clone, Debug)]
pub struct InkPen {
pub color: String,
pub width: u32,
pub tip: PenTip,
pub opacity: f32,
}
impl InkPen {
pub fn new(color: &str, width: u32) -> Self {
Self {
color: color.trim_start_matches('#').to_uppercase(),
width,
tip: PenTip::default(),
opacity: 1.0,
}
}
pub fn tip(mut self, tip: PenTip) -> Self {
self.tip = tip;
self
}
pub fn opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity.clamp(0.0, 1.0);
self
}
pub fn red() -> Self {
Self::new("FF0000", 50)
}
pub fn blue() -> Self {
Self::new("0000FF", 50)
}
pub fn black() -> Self {
Self::new("000000", 50)
}
pub fn highlighter() -> Self {
Self::new("FFFF00", 300).tip(PenTip::Flat).opacity(0.5)
}
}
#[derive(Clone, Debug, Copy, PartialEq)]
pub struct InkPoint {
pub x: f64,
pub y: f64,
}
impl InkPoint {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
#[derive(Clone, Debug)]
pub struct InkStroke {
pub points: Vec<InkPoint>,
pub pen: InkPen,
}
impl InkStroke {
pub fn new(pen: InkPen) -> Self {
Self {
points: Vec::new(),
pen,
}
}
pub fn add_point(mut self, x: f64, y: f64) -> Self {
self.points.push(InkPoint::new(x, y));
self
}
pub fn add_points(mut self, points: &[(f64, f64)]) -> Self {
for &(x, y) in points {
self.points.push(InkPoint::new(x, y));
}
self
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
fn points_str(&self) -> String {
self.points
.iter()
.map(|p| format!("{} {}", p.x as i64, p.y as i64))
.collect::<Vec<_>>()
.join(" ")
}
pub fn to_xml(&self, trace_id: u32) -> String {
format!(
"<ink:trace contextRef=\"#{}\" brushRef=\"#br{}\" id=\"{}\">{}</ink:trace>",
trace_id,
trace_id,
trace_id,
self.points_str(),
)
}
}
#[derive(Clone, Debug, Default)]
pub struct InkAnnotations {
strokes: Vec<InkStroke>,
}
impl InkAnnotations {
pub fn new() -> Self {
Self::default()
}
pub fn add_stroke(&mut self, stroke: InkStroke) {
self.strokes.push(stroke);
}
pub fn strokes(&self) -> &[InkStroke] {
&self.strokes
}
pub fn len(&self) -> usize {
self.strokes.len()
}
pub fn is_empty(&self) -> bool {
self.strokes.is_empty()
}
pub fn clear(&mut self) {
self.strokes.clear();
}
pub fn to_xml(&self) -> String {
if self.strokes.is_empty() {
return String::new();
}
let mut xml = String::from(
r#"<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice Requires="p14"><p:contentPart xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
);
xml.push_str(r#"<ink:ink xmlns:ink="http://www.w3.org/2003/InkML">"#);
for (i, stroke) in self.strokes.iter().enumerate() {
let opacity_attr = if (stroke.pen.opacity - 1.0).abs() > 0.01 {
format!(r#" transparency="{:.0}""#, (1.0 - stroke.pen.opacity) * 255.0)
} else {
String::new()
};
xml.push_str(&format!(
"<ink:brush id=\"br{}\" color=\"#{}\" width=\"{}\" tip=\"{}\"{}/>\n",
i, stroke.pen.color, stroke.pen.width, stroke.pen.tip.to_xml(), opacity_attr,
));
}
for (i, stroke) in self.strokes.iter().enumerate() {
xml.push_str(&stroke.to_xml(i as u32));
}
xml.push_str("</ink:ink>");
xml.push_str("</p:contentPart></mc:Choice></mc:AlternateContent>");
xml
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pen_tip_default() {
assert_eq!(PenTip::default(), PenTip::Ball);
assert_eq!(PenTip::Ball.to_xml(), "ball");
assert_eq!(PenTip::Flat.to_xml(), "flat");
}
#[test]
fn test_ink_pen_new() {
let pen = InkPen::new("FF0000", 50);
assert_eq!(pen.color, "FF0000");
assert_eq!(pen.width, 50);
assert_eq!(pen.tip, PenTip::Ball);
assert!((pen.opacity - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_ink_pen_presets() {
let red = InkPen::red();
assert_eq!(red.color, "FF0000");
let blue = InkPen::blue();
assert_eq!(blue.color, "0000FF");
let black = InkPen::black();
assert_eq!(black.color, "000000");
}
#[test]
fn test_ink_pen_highlighter() {
let h = InkPen::highlighter();
assert_eq!(h.color, "FFFF00");
assert_eq!(h.tip, PenTip::Flat);
assert!((h.opacity - 0.5).abs() < f32::EPSILON);
assert_eq!(h.width, 300);
}
#[test]
fn test_ink_pen_opacity_clamp() {
let pen = InkPen::new("000000", 10).opacity(2.0);
assert!((pen.opacity - 1.0).abs() < f32::EPSILON);
let pen2 = InkPen::new("000000", 10).opacity(-1.0);
assert!((pen2.opacity - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_ink_point() {
let p = InkPoint::new(100.0, 200.0);
assert!((p.x - 100.0).abs() < f64::EPSILON);
assert!((p.y - 200.0).abs() < f64::EPSILON);
}
#[test]
fn test_ink_stroke_new() {
let stroke = InkStroke::new(InkPen::black());
assert!(stroke.is_empty());
assert_eq!(stroke.len(), 0);
}
#[test]
fn test_ink_stroke_add_points() {
let stroke = InkStroke::new(InkPen::red())
.add_point(0.0, 0.0)
.add_point(100.0, 100.0)
.add_point(200.0, 50.0);
assert_eq!(stroke.len(), 3);
assert!(!stroke.is_empty());
}
#[test]
fn test_ink_stroke_add_points_batch() {
let stroke = InkStroke::new(InkPen::blue())
.add_points(&[(0.0, 0.0), (50.0, 50.0), (100.0, 0.0)]);
assert_eq!(stroke.len(), 3);
}
#[test]
fn test_ink_stroke_xml() {
let stroke = InkStroke::new(InkPen::black())
.add_point(10.0, 20.0)
.add_point(30.0, 40.0);
let xml = stroke.to_xml(0);
assert!(xml.contains("ink:trace"));
assert!(xml.contains("10 20"));
assert!(xml.contains("30 40"));
}
#[test]
fn test_ink_annotations_new() {
let ann = InkAnnotations::new();
assert!(ann.is_empty());
assert_eq!(ann.len(), 0);
}
#[test]
fn test_ink_annotations_add() {
let mut ann = InkAnnotations::new();
ann.add_stroke(InkStroke::new(InkPen::red()).add_point(0.0, 0.0));
ann.add_stroke(InkStroke::new(InkPen::blue()).add_point(10.0, 10.0));
assert_eq!(ann.len(), 2);
}
#[test]
fn test_ink_annotations_clear() {
let mut ann = InkAnnotations::new();
ann.add_stroke(InkStroke::new(InkPen::black()).add_point(0.0, 0.0));
ann.clear();
assert!(ann.is_empty());
}
#[test]
fn test_ink_annotations_xml_empty() {
let ann = InkAnnotations::new();
assert_eq!(ann.to_xml(), "");
}
#[test]
fn test_ink_annotations_xml() {
let mut ann = InkAnnotations::new();
ann.add_stroke(
InkStroke::new(InkPen::red())
.add_point(0.0, 0.0)
.add_point(100.0, 100.0),
);
let xml = ann.to_xml();
assert!(xml.contains("mc:AlternateContent"));
assert!(xml.contains("ink:ink"));
assert!(xml.contains("ink:brush"));
assert!(xml.contains("ink:trace"));
assert!(xml.contains("FF0000"));
}
#[test]
fn test_ink_annotations_xml_highlighter() {
let mut ann = InkAnnotations::new();
ann.add_stroke(
InkStroke::new(InkPen::highlighter())
.add_point(0.0, 0.0)
.add_point(500.0, 0.0),
);
let xml = ann.to_xml();
assert!(xml.contains("FFFF00"));
assert!(xml.contains("flat"));
assert!(xml.contains("transparency"));
}
}