use serde::{Deserialize, Serialize};
pub enum Clipboard {
ImageData(ImageClip),
Layer(LayerClipboard),
}
impl Clipboard {
pub fn as_image(&self) -> Option<&ImageClip> {
match self {
Clipboard::ImageData(clip) => Some(clip),
Clipboard::Layer(_) => None,
}
}
pub fn as_layer(&self) -> Option<&LayerClipboard> {
match self {
Clipboard::Layer(l) => Some(l),
_ => None,
}
}
}
pub struct ImageClip {
pub data: Vec<u8>,
pub width: u32,
pub height: u32,
pub offset_x: i32,
pub offset_y: i32,
}
impl ImageClip {
pub fn from_rgba(width: u32, height: u32, rgba: Vec<u8>, offset_x: i32, offset_y: i32) -> Self {
debug_assert_eq!(rgba.len(), (width * height * 4) as usize);
ImageClip {
data: rgba,
width,
height,
offset_x,
offset_y,
}
}
pub fn to_rgba(&self) -> (&[u8], u32, u32, i32, i32) {
(
&self.data,
self.width,
self.height,
self.offset_x,
self.offset_y,
)
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
}
pub const LAYER_CLIPBOARD_SCHEMA_VERSION: u32 = 1;
#[derive(Serialize, Deserialize, Clone)]
pub struct LayerClipboard {
pub schema_version: u32,
pub name: String,
pub visible: bool,
pub locked: bool,
pub opacity: f32,
pub blend_mode: String,
pub bounds: ClipboardRect,
pub pixels_b64: String,
pub mask: Option<MaskClipboard>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct MaskClipboard {
pub name: String,
pub visible: bool,
pub bounds: ClipboardRect,
#[serde(default)]
pub pixels_b64: String,
}
#[derive(Serialize, Deserialize, Clone, Copy)]
pub struct ClipboardRect {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
impl LayerClipboard {
pub fn decode_pixels(&self) -> Result<Vec<u8>, base64::DecodeError> {
use base64::{engine::general_purpose::STANDARD, Engine as _};
STANDARD.decode(&self.pixels_b64)
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).expect("LayerClipboard serializes infallibly")
}
pub fn from_json(s: &str) -> Result<Self, String> {
let parsed: LayerClipboard = serde_json::from_str(s).map_err(|e| e.to_string())?;
if parsed.schema_version > LAYER_CLIPBOARD_SCHEMA_VERSION {
return Err(format!(
"LayerClipboard schema_version {} is newer than this build's {}",
parsed.schema_version, LAYER_CLIPBOARD_SCHEMA_VERSION
));
}
Ok(parsed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_rgba() {
let w = 4u32;
let h = 4u32;
let mut rgba = vec![0u8; (w * h * 4) as usize];
for i in 0..16 {
rgba[i * 4] = 255; rgba[i * 4 + 3] = 255; }
let clip = ImageClip::from_rgba(w, h, rgba.clone(), 10, 20);
assert_eq!(clip.width, 4);
assert_eq!(clip.height, 4);
assert_eq!(clip.offset_x, 10);
assert_eq!(clip.offset_y, 20);
let (out, ow, oh, ox, oy) = clip.to_rgba();
assert_eq!((ow, oh), (4, 4));
assert_eq!((ox, oy), (10, 20));
assert_eq!(out[0], 255); assert_eq!(out[1], 0); assert_eq!(out[2], 0); assert_eq!(out[3], 255); }
#[test]
fn empty_clip() {
let clip = ImageClip::from_rgba(0, 0, vec![], 0, 0);
assert!(clip.is_empty());
}
#[test]
fn layer_clipboard_roundtrips_through_json() {
use base64::{engine::general_purpose::STANDARD, Engine as _};
let clip = LayerClipboard {
schema_version: LAYER_CLIPBOARD_SCHEMA_VERSION,
name: "Painted layer".into(),
visible: true,
locked: false,
opacity: 0.65,
blend_mode: "multiply".into(),
bounds: ClipboardRect {
x: 12,
y: -4,
width: 8,
height: 4,
},
pixels_b64: STANDARD.encode([0xAA; 8 * 4 * 4]),
mask: Some(MaskClipboard {
name: "Mask".into(),
visible: true,
bounds: ClipboardRect {
x: 12,
y: -4,
width: 8,
height: 4,
},
pixels_b64: String::new(),
}),
};
let json = clip.to_json();
let back = LayerClipboard::from_json(&json).unwrap();
assert_eq!(back.schema_version, LAYER_CLIPBOARD_SCHEMA_VERSION);
assert_eq!(back.name, "Painted layer");
assert!((back.opacity - 0.65).abs() < 1e-6);
assert_eq!(back.blend_mode, "multiply");
assert_eq!(back.bounds.width, 8);
assert_eq!(back.bounds.x, 12);
assert_eq!(back.decode_pixels().unwrap().len(), 8 * 4 * 4);
assert!(back.mask.is_some());
assert_eq!(back.mask.unwrap().name, "Mask");
}
#[test]
fn rejects_future_schema_version() {
let json = serde_json::json!({
"schema_version": LAYER_CLIPBOARD_SCHEMA_VERSION + 1,
"name": "x", "visible": true, "locked": false,
"opacity": 1.0, "blend_mode": "normal",
"bounds": {"x":0, "y":0, "width":1, "height":1},
"pixels_b64": "",
"mask": null,
})
.to_string();
assert!(LayerClipboard::from_json(&json).is_err());
}
}