krita 0.2.1

Parser for Krita files
Documentation
// Copyright (c) 2020 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//! Crate aiming at parsing Krita layers.

use core::cell::RefCell;
use std::sync::Mutex;
use std::fs::File;
use std::io::{self, Read};
use std::fmt;
use std::path::Path;
use std::str::FromStr;
use zip::ZipArchive;
use minidom::Element;
use crate::paint_layer::PaintLayer;

#[derive(Debug, Clone, Copy)]
pub enum NodeType {
    ShapeLayer,
    PaintLayer,
}

impl FromStr for NodeType {
    type Err = String;

    fn from_str(s: &str) -> Result<NodeType, Self::Err> {
        Ok(match s {
            "shapelayer" => NodeType::ShapeLayer,
            "paintlayer" => NodeType::PaintLayer,
            other => return Err(String::from(other)),
        })
    }
}

#[derive(Debug, Clone, Copy)]
pub enum CompositeOp {
    Normal,
}

impl FromStr for CompositeOp {
    type Err = String;

    fn from_str(s: &str) -> Result<CompositeOp, Self::Err> {
        Ok(match s {
            "normal" => CompositeOp::Normal,
            other => return Err(String::from(other)),
        })
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Colorspace {
    Rgba,
}

impl FromStr for Colorspace {
    type Err = String;

    fn from_str(s: &str) -> Result<Colorspace, Self::Err> {
        Ok(match s {
            "RGBA" => Colorspace::Rgba,
            other => return Err(String::from(other)),
        })
    }
}

impl fmt::Display for Colorspace {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        write!(fmt, "{}", match self {
            Colorspace::Rgba => "RGBA",
        })
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum ParsedLayer {
    PaintLayer([u8; 4], PaintLayer),
    ShapeLayer(String),
}

#[derive(Debug, Clone)]
pub struct Layer {
    name: String,
    filename: String,
    uuid: String,
    colorspace: Option<Colorspace>,
    channelflags: String,
    channellockflags: Option<String>,
    nodetype: NodeType,
    compositeop: CompositeOp,
    opacity: u8,
    colorlabel: bool,
    selected: bool,
    intimeline: bool,
    locked: bool,
    visible: bool,
    collapsed: bool,
    onionskin: Option<u8>,
    x: usize,
    y: usize,
}

impl Layer {
    pub fn get_name(&self) -> &str {
        &self.name
    }

    pub fn get_position(&self) -> (usize, usize) {
        (self.x, self.y)
    }
}

#[derive(Debug, Clone)]
struct Image {
    name: String,
    description: String,
    profile: String,
    colorspace: Colorspace,
    width: usize,
    height: usize,
    x_res: usize,
    y_res: usize,
    layers: Vec<Layer>,
}

#[derive(Debug)]
/// The main struct of this crate, it contains a header and at least one tile.
pub struct ParsedKra {
    zip: Mutex<RefCell<ZipArchive<File>>>,
    image: Image,
}

#[derive(Debug, Clone)]
pub struct Error {
}

impl ParsedKra {
    pub fn get_name(&self) -> &str {
        &self.image.name
    }

    pub fn get_description(&self) -> &str {
        &self.image.description
    }

    pub fn get_profile(&self) -> &str {
        &self.image.profile
    }

    pub fn get_colorspace(&self) -> Colorspace {
        self.image.colorspace
    }

    pub fn get_dimensions(&self) -> (usize, usize) {
        (self.image.width, self.image.height)
    }

    pub fn get_res(&self) -> (usize, usize) {
        (self.image.x_res, self.image.y_res)
    }

    fn get_paint_layer(layer: &Layer, zip: &mut ZipArchive<File>, name: &str) -> io::Result<ParsedLayer> {
        let default_pixel = {
            let mut file = zip.by_name(&format!("{}/layers/{}.defaultpixel", name, layer.filename))?;
            let mut content = Vec::new();
            file.read_to_end(&mut content)?;
            match content.as_slice() {
                [r, g, b, a] => [r.clone(), g.clone(), b.clone(), a.clone()],
                _ => return Err(io::Error::new(io::ErrorKind::Other, "wrong defaultpixel")),
            }
        };
        let paint_layer = {
            let mut file = zip.by_name(&format!("{}/layers/{}", name, layer.filename))?;
            let mut content = Vec::new();
            file.read_to_end(&mut content)?;
            PaintLayer::parse(&content).unwrap().1
        };
        // TODO: also parse the icc file.
        Ok(ParsedLayer::PaintLayer(default_pixel, paint_layer))
    }

    fn get_shape_layer(layer: &Layer, zip: &mut ZipArchive<File>, name: &str) -> io::Result<ParsedLayer> {
        let mut file = zip.by_name(&format!("{}/layers/{}.shapelayer/content.svg", name, layer.filename))?;
        let mut content = String::new();
        file.read_to_string(&mut content)?;
        // TODO: do something with the SVG file.
        Ok(ParsedLayer::ShapeLayer(content))
    }

    pub fn parse_layer(&self, layer: &Layer) -> io::Result<ParsedLayer> {
        let zip = self.zip.lock().unwrap();
        let mut zip = zip.borrow_mut();
        Ok(match layer.nodetype {
            NodeType::PaintLayer => ParsedKra::get_paint_layer(layer, &mut zip, &self.image.name)?,
            NodeType::ShapeLayer => ParsedKra::get_shape_layer(layer, &mut zip, &self.image.name)?,
        })
    }

    pub fn len(&self) -> usize {
        self.image.layers.len()
    }

    pub fn iter(&self) -> impl Iterator<Item = &Layer> {
        self.image.layers.iter().rev()
    }
}

#[derive(Debug)]
/// The main struct of this crate, it contains a header and at least one tile.
pub struct Kra {
    zip: ZipArchive<File>,
}

impl Kra {
    /// Parse a Krita .kra file.
    pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Kra> {
        let file = File::open(path)?;
        let mut zip = ZipArchive::new(file)?;
        {
            let mut file = zip.by_name("mimetype")?;
            let mut mimetype = String::new();
            file.read_to_string(&mut mimetype)?;
            if mimetype != "application/x-krita" {
                return Err(io::Error::new(io::ErrorKind::Other, "not a Krita file"));
            }
        }
        Ok(Kra {
            zip,
        })
    }

    pub fn read_maindoc(mut self) -> io::Result<ParsedKra> {
        let mut string = String::new();
        {
            let mut file = self.zip.by_name("maindoc.xml")?;
            file.read_to_string(&mut string)?;
        }
        let doc: Element = string.parse().unwrap();
        let image = Kra::parse_maindoc(&doc)?;

        Ok(ParsedKra {
            zip: Mutex::new(RefCell::new(self.zip)),
            image,
        })
    }

    fn parse_maindoc(doc: &Element) -> io::Result<Image> {
        assert!(doc.is("DOC", "http://www.calligra.org/DTD/krita"));
        assert_eq!(doc.attr("syntaxVersion"), Some("2"));

        let image = doc.get_child("IMAGE", "http://www.calligra.org/DTD/krita").unwrap();
        Ok(Kra::parse_image(image)?)
    }

    fn parse_image(image: &Element) -> io::Result<Image> {
        assert!(image.is("IMAGE", "http://www.calligra.org/DTD/krita"));
        assert_eq!(image.attr("mime").unwrap(), "application/x-kra");
        let name = image.attr("name").unwrap().to_owned();
        let description = image.attr("description").unwrap().to_owned();
        let profile = image.attr("profile").unwrap().to_owned();
        let colorspace = image.attr("colorspacename").map(|x| x.parse().unwrap()).unwrap();
        let width = image.attr("width").map(|x| x.parse().unwrap()).unwrap();
        let height = image.attr("height").map(|x| x.parse().unwrap()).unwrap();
        let x_res = image.attr("x-res").map(|x| x.parse().unwrap()).unwrap();
        let y_res = image.attr("y-res").map(|x| x.parse().unwrap()).unwrap();

        Ok(Image {
            name,
            description,
            profile,
            colorspace,
            width,
            height,
            x_res,
            y_res,
            layers: image
                .get_child("layers", "http://www.calligra.org/DTD/krita")
                .unwrap()
                .children()
                .map(Kra::parse_layer)
                .collect::<Result<_, _>>()?,
        })
    }

    fn parse_layer(layer: &Element) -> io::Result<Layer> {
        assert!(layer.is("layer", "http://www.calligra.org/DTD/krita"));
        let name = layer.attr("name").unwrap().to_owned();
        let filename = layer.attr("filename").unwrap().to_owned();
        let uuid = layer.attr("uuid").unwrap().to_owned();
        let colorspace = layer.attr("colorspacename").map(|x| x.parse().unwrap());
        let channelflags = layer.attr("channelflags").unwrap().to_owned();
        let channellockflags = layer.attr("channellockflags").map(str::to_owned);
        let nodetype = layer.attr("nodetype").map(|x| x.parse().unwrap()).unwrap();
        let compositeop = layer.attr("compositeop").map(|x| x.parse().unwrap()).unwrap();
        let opacity = layer.attr("opacity").map(|x| x.parse().unwrap()).unwrap();
        let colorlabel = layer.attr("colorlabel").map(|x| x.parse::<u8>().unwrap()).unwrap() != 0;
        let selected = layer.attr("selected") == Some("true");
        let intimeline = layer.attr("intimeline").map(|x| x.parse::<u8>().unwrap()).unwrap() != 0;
        let locked = layer.attr("locked").map(|x| x.parse::<u8>().unwrap()).unwrap() != 0;
        let visible = layer.attr("visible").map(|x| x.parse::<u8>().unwrap()).unwrap() != 0;
        let collapsed = layer.attr("collapsed").map(|x| x.parse::<u8>().unwrap()).unwrap() != 0;
        let onionskin = layer.attr("onionskin").map(|x| x.parse::<u8>().unwrap());
        let x = layer.attr("x").map(|x| x.parse().unwrap()).unwrap();
        let y = layer.attr("y").map(|x| x.parse().unwrap()).unwrap();

        Ok(Layer {
            name,
            filename,
            uuid,
            colorspace,
            channelflags,
            channellockflags,
            nodetype,
            compositeop,
            opacity,
            colorlabel,
            selected,
            intimeline,
            locked,
            visible,
            collapsed,
            onionskin,
            x,
            y,
        })
    }
}