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/.

//! Parser for paint layers, raster images split in tiles.

use nom::{
    IResult,
    bytes::complete::tag,
    sequence::{tuple, delimited},
    character::complete::char,
    bytes::complete::is_not,
    multi::count,
};

#[derive(Debug, Clone, PartialEq)]
/// The representation of a paint layer, it contains a header and at least one tile.
pub struct PaintLayer {
    header: Header,

    /// The tiles composing this layer.
    pub tiles: Vec<Tile>,
}

#[derive(Debug, Clone, PartialEq)]
struct Header {
    version: u8,
    tile_width: usize,
    tile_height: usize,
    pixel_size: usize,
    num_tiles: usize,
}

#[derive(Debug, Clone, PartialEq)]
/// A tile is a 64×64 planar image, placed at a given position.
pub struct Tile {
    /// The x position of the beginning of the tile, from the top-left origin.
    pub x: usize,

    /// The y position of the beginning of the tile, from the top-left origin.
    pub y: usize,

    /// The width of this tile.
    pub width: usize,

    /// The height of this tile.
    pub height: usize,

    /// The size of a pixel, in bytes.
    pub pixel_size: usize,

    /// The uncompressed data of the tile.  This is still planar.
    pub data: Vec<u8>,
}

#[derive(Debug, PartialEq)]
/// The compression method used for a given tile.
enum Compression {
    /// LZF is the only supported compression method.
    Lzf,
}

fn parse_usize(i: &[u8]) -> IResult<&[u8], usize> {
    match lexical_core::parse_partial(i) {
        Ok((value, processed)) => Ok((&i[processed..], value)),
        // TODO: no panic!
        Err(err) => panic!("parse_usize error: {:?}", err),
    }
}

fn parse_compression(i: &[u8]) -> IResult<&[u8], Compression> {
    tag("LZF")(i).map(|(i, _lzf)| (i, Compression::Lzf))
}

fn parse_header(input: &[u8]) -> IResult<&[u8], Header> {
    let (input, (version, tile_width, tile_height, pixel_size, num_tiles)) = tuple((
        delimited(tag("VERSION "), is_not("\n"), char('\n')),
        delimited(tag("TILEWIDTH "), is_not("\n"), char('\n')),
        delimited(tag("TILEHEIGHT "), is_not("\n"), char('\n')),
        delimited(tag("PIXELSIZE "), is_not("\n"), char('\n')),
        delimited(tag("DATA "), is_not("\n"), char('\n')),
    ))(input)?;

    // TODO: no unwrap!
    let version = lexical_core::parse(version).unwrap();
    let tile_width = lexical_core::parse(tile_width).unwrap();
    let tile_height = lexical_core::parse(tile_height).unwrap();
    let pixel_size = lexical_core::parse(pixel_size).unwrap();
    let num_tiles = lexical_core::parse(num_tiles).unwrap();

    Ok((input, Header {
        version,
        tile_width,
        tile_height,
        pixel_size,
        num_tiles,
    }))
}

fn parse_tile(width: usize, height: usize, pixel_size: usize) -> impl Fn(&[u8]) -> IResult<&[u8], Tile> {
    move |i| {
        let (i, (x, _, y, _, _compression, _, length, _)) = tuple((
            parse_usize,
            char(','),
            parse_usize,
            char(','),
            parse_compression,
            char(','),
            parse_usize,
            char('\n'),
        ))(i)?;
        // TODO: figure out why this 1.. instead of starting at the beginning.
        // TODO: no unwrap!
        let data = lzf::decompress(&i[1..length], width * height * pixel_size).unwrap();
        let tile = Tile {
            x,
            y,
            width,
            height,
            pixel_size,
            data,
        };
        Ok((&i[length..], tile))
    }
}

impl PaintLayer {
    /// Parse a Krita layer, feed it the data found inside a .kra file.
    pub fn parse(input: &[u8]) -> IResult<&[u8], PaintLayer> {
        let i = input;
        let (i, header) = parse_header(i)?;
        let (i, tiles) = count(parse_tile(header.tile_width, header.tile_height, header.pixel_size), header.num_tiles)(i)?;
        let layer = PaintLayer {
            header,
            tiles,
        };
        Ok((i, layer))
    }

    /// Blit all tiles of this layer to a single image.
    ///
    /// Additionally performs a planar to linear conversion for easier use.
    pub fn assemble_tiles(&self, default_pixel: [u8; 4], width: usize, height: usize) -> Vec<u8> {
        let mut pixels = Vec::with_capacity(width * height * 4);
        unsafe { pixels.set_len(width * height * 4) };
        for i in 0..(width * height) {
            pixels[i * 4    ] = default_pixel[0];
            pixels[i * 4 + 1] = default_pixel[1];
            pixels[i * 4 + 2] = default_pixel[2];
            pixels[i * 4 + 3] = default_pixel[3];
        }
        for tile in &self.tiles {
            let x = tile.x;
            let y = tile.y;
            let nb_pixels = tile.width * tile.height;
            let data = &tile.data;
            for y in y..(y + tile.height) {
                // TODO: figure out why this is required.
                if y >= height {
                    continue;
                }
                for x in x..(x + tile.width) {
                    pixels[(y * width + x) * 4    ] = data[2 * nb_pixels + tile.width * (y % tile.height) + (x % tile.width)];
                    pixels[(y * width + x) * 4 + 1] = data[    nb_pixels + tile.width * (y % tile.height) + (x % tile.width)];
                    pixels[(y * width + x) * 4 + 2] = data[                tile.width * (y % tile.height) + (x % tile.width)];
                    pixels[(y * width + x) * 4 + 3] = data[3 * nb_pixels + tile.width * (y % tile.height) + (x % tile.width)];
                }
            }
        }
        pixels
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn header() {
        let header = parse_header(b"VERSION 2\nTILEWIDTH 64\nTILEHEIGHT 64\nPIXELSIZE 4\nDATA 240\n").unwrap().1;
        assert_eq!(header.version, 2);
        assert_eq!(header.tile_width, 64);
        assert_eq!(header.tile_height, 64);
        assert_eq!(header.pixel_size, 4);
        assert_eq!(header.num_tiles, 240);
    }
}