Task 344: .KRA File Format
Task 344: .KRA File Format
1. List of Properties of the .KRA File Format Intrinsic to Its File System
The .KRA file format is a ZIP archive used by Krita for saving digital paintings. It contains XML metadata, binary layer data, and image previews. Based on the format specifications, the intrinsic properties (structural and metadata fields embedded in the file's internal structure, such as headers, XML attributes, and binary metadata) are:
- Document Width: The width of the canvas in pixels, defined in maindoc.xml under the element.
- Document Height: The height of the canvas in pixels, defined in maindoc.xml under the element.
- Color Space Name: The color model (e.g., "RGBA", "GRAYA"), defined in maindoc.xml under and elements.
- Bit Depth: Implied from pixel size (e.g., 4 bytes for RGBA8), calculated from layer binary headers.
- Version: The format version of layer data (e.g., "VERSION 2"), found in layer binary files.
- Tile Width: The width of data tiles (e.g., 64), found in layer binary files.
- Tile Height: The height of data tiles (e.g., 64), found in layer binary files.
- Pixel Size: Bytes per pixel (e.g., 4 for RGBA, 1 for mask), found in layer binary files.
- Compression Type: The compression method for tile data (e.g., "LZF" or uncompressed), found in layer binary files.
- Number of Layers: The count of layers, derived from the number of elements in maindoc.xml or layer files in the ZIP.
- Has Animation: Boolean indicating if animation keyframes are present (checks for keyframes.xml or .fX files).
- ICC Profile: Presence and content of per-layer ICC profiles (in .icc files).
- Default Pixel: The default pixel value for layers (e.g., \x00\x00\x00\x00 for RGBA8), in .defaultpixel files.
These properties are extracted from the ZIP contents, primarily maindoc.xml and layer binary headers.
2. Two Direct Download Links for .KRA Files
Here are two direct download links to sample .KRA files from the Pepper&Carrot webcomic (CC-BY licensed, created in Krita):
- https://www.peppercarrot.com/0_sources/ep01_Potion-of-Flight/hi-res/en_Pepper-and-Carrot_by-David-Revoy_E01.kra
- https://www.peppercarrot.com/0_sources/ep36_The-Surprise-Attack/hi-res/en_Pepper-and-Carrot_by-David-Revoy_E36.kra
3. Ghost Blog Embedded HTML JavaScript for Drag and Drop .KRA File Dump
Here's a self-contained HTML snippet with JavaScript that can be embedded in a blog (e.g., Ghost blog). It allows dragging and dropping a .KRA file, unzips it using JSZip, parses maindoc.xml and a sample layer file, extracts the properties, and dumps them to the screen.
4. Python Class for .KRA File
import zipfile
import xml.etree.ElementTree as ET
import os
class KraHandler:
def __init__(self, filepath):
self.filepath = filepath
self.properties = {}
self.zip = None
def open(self):
self.zip = zipfile.ZipFile(self.filepath, 'r')
def decode_read(self):
if not self.zip:
self.open()
# Read maindoc.xml
with self.zip.open('maindoc.xml') as f:
tree = ET.parse(f)
root = tree.getroot()
image = root.find('IMAGE')
self.properties['Document Width'] = image.attrib['width']
self.properties['Document Height'] = image.attrib['height']
self.properties['Color Space Name'] = image.attrib['colorspacename']
self.properties['Number of Layers'] = len(root.findall('.//layer'))
# Find first layer file
layer_files = [f for f in self.zip.namelist() if f.endswith('layer1')] # Assume layer1
if layer_files:
with self.zip.open(layer_files[0]) as f:
lines = f.read().decode('utf-8').splitlines()
self.properties['Version'] = lines[0].replace('VERSION ', '')
self.properties['Tile Width'] = lines[1].replace('TILEWIDTH ', '')
self.properties['Tile Height'] = lines[2].replace('TILEHEIGHT ', '')
self.properties['Pixel Size'] = lines[3].replace('PIXELSIZE ', '')
data_line = lines[4]
self.properties['Compression Type'] = data_line.split(',')[2] if ',' in data_line else 'Uncompressed'
self.properties['Bit Depth'] = str(int(self.properties['Pixel Size']) * 8 // 4) # Simplified
self.properties['Has Animation'] = 'Yes' if any('keyframes.xml' in f for f in self.zip.namelist()) else 'No'
self.properties['ICC Profile'] = 'Present' if any('.icc' in f for f in self.zip.namelist()) else 'Absent'
default_pixel_files = [f for f in self.zip.namelist() if f.endswith('.defaultpixel')]
self.properties['Default Pixel'] = self.zip.read(default_pixel_files[0]) if default_pixel_files else b'N/A'
def print_properties(self):
for k, v in self.properties.items():
print(f"{k}: {v}")
def write(self, new_filepath):
# For write, create a new ZIP with modified properties (simple example: copy and modify maindoc.xml)
with zipfile.ZipFile(new_filepath, 'w') as new_zip:
for item in self.zip.infolist():
data = self.zip.read(item.filename)
if item.filename == 'maindoc.xml':
# Example modification: change width
tree = ET.parse(data)
image = tree.find('IMAGE')
image.attrib['width'] = str(int(image.attrib['width']) + 10) # Arbitrary change
data = ET.tostring(tree.getroot())
new_zip.writestr(item, data)
def close(self):
if self.zip:
self.zip.close()
# Usage example
if __name__ == '__main__':
handler = KraHandler('example.kra')
handler.open()
handler.decode_read()
handler.print_properties()
handler.write('modified.kra')
handler.close()
5. Java Class for .KRA File
import java.io.*;
import java.util.zip.*;
import org.w3c.dom.*;
import javax.xml.parsers.*;
public class KraHandler {
private String filepath;
private ZipFile zip;
private java.util.Map<String, String> properties = new java.util.HashMap<>();
public KraHandler(String filepath) {
this.filepath = filepath;
}
public void open() throws IOException {
zip = new ZipFile(filepath);
}
public void decodeRead() throws Exception {
if (zip == null) open();
// Read maindoc.xml
ZipEntry entry = zip.getEntry("maindoc.xml");
InputStream is = zip.getInputStream(entry);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(is);
Element image = (Element) doc.getElementsByTagName("IMAGE").item(0);
properties.put("Document Width", image.getAttribute("width"));
properties.put("Document Height", image.getAttribute("height"));
properties.put("Color Space Name", image.getAttribute("colorspacename"));
properties.put("Number of Layers", String.valueOf(doc.getElementsByTagName("layer").getLength()));
// Find first layer file
java.util.Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry e = entries.nextElement();
if (e.getName().matches(".*/layer\\d+$")) {
BufferedReader br = new BufferedReader(new InputStreamReader(zip.getInputStream(e)));
String line = br.readLine();
properties.put("Version", line.replace("VERSION ", ""));
line = br.readLine();
properties.put("Tile Width", line.replace("TILEWIDTH ", ""));
line = br.readLine();
properties.put("Tile Height", line.replace("TILEHEIGHT ", ""));
line = br.readLine();
properties.put("Pixel Size", line.replace("PIXELSIZE ", ""));
line = br.readLine();
String[] parts = line.split(",");
properties.put("Compression Type", parts.length > 2 ? parts[2] : "Uncompressed");
break;
}
}
properties.put("Bit Depth", String.valueOf(Integer.parseInt(properties.get("Pixel Size")) * 8 / 4));
properties.put("Has Animation", zip.getEntry("keyframes.xml") != null ? "Yes" : "No");
properties.put("ICC Profile", zip.getEntry(".icc") != null ? "Present" : "Absent");
// Default pixel
entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry e = entries.nextElement();
if (e.getName().endsWith(".defaultpixel")) {
InputStream dis = zip.getInputStream(e);
properties.put("Default Pixel", new String(dis.readAllBytes()));
break;
}
}
}
public void printProperties() {
properties.forEach((k, v) -> System.out.println(k + ": " + v));
}
public void write(String newFilepath) throws Exception {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(newFilepath));
java.util.Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry e = entries.nextElement();
zos.putNextEntry(new ZipEntry(e.getName()));
InputStream is = zip.getInputStream(e);
byte[] buffer = new byte[1024];
int len;
if (e.getName().equals("maindoc.xml")) {
// Modify example: change width
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(is);
Element image = (Element) doc.getElementsByTagName("IMAGE").item(0);
image.setAttribute("width", String.valueOf(Integer.parseInt(image.getAttribute("width")) + 10));
javax.xml.transform.TransformerFactory tf = javax.xml.transform.TransformerFactory.newInstance();
javax.xml.transform.Transformer t = tf.newTransformer();
javax.xml.transform.dom.DOMSource source = new javax.xml.transform.dom.DOMSource(doc);
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
t.transform(source, new javax.xml.transform.stream.StreamResult(baos));
baos.writeTo(zos);
} else {
while ((len = is.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
}
zos.closeEntry();
}
zos.close();
}
public void close() throws IOException {
if (zip != null) zip.close();
}
public static void main(String[] args) throws Exception {
KraHandler handler = new KraHandler("example.kra");
handler.open();
handler.decodeRead();
handler.printProperties();
handler.write("modified.kra");
handler.close();
}
}
6. JavaScript Class for .KRA File
This is for Node.js (requires 'fs' and 'adm-zip' module; install via npm).
const fs = require('fs');
const AdmZip = require('adm-zip');
const { DOMParser } = require('xmldom');
class KraHandler {
constructor(filepath) {
this.filepath = filepath;
this.properties = {};
this.zip = null;
}
open() {
this.zip = new AdmZip(this.filepath);
}
decodeRead() {
if (!this.zip) this.open();
// Read maindoc.xml
const maindoc = this.zip.readAsText('maindoc.xml');
const parser = new DOMParser();
const xml = parser.parseFromString(maindoc, 'application/xml');
const image = xml.getElementsByTagName('IMAGE')[0];
this.properties['Document Width'] = image.getAttribute('width');
this.properties['Document Height'] = image.getAttribute('height');
this.properties['Color Space Name'] = image.getAttribute('colorspacename');
this.properties['Number of Layers'] = xml.getElementsByTagName('layer').length;
// Find first layer file
const layerFiles = this.zip.getEntries().filter(e => e.entryName.match(/layers\/layer\d+$/));
if (layerFiles.length > 0) {
const layerData = this.zip.readAsText(layerFiles[0]);
const lines = layerData.split('\n');
this.properties['Version'] = lines[0].replace('VERSION ', '');
this.properties['Tile Width'] = lines[1].replace('TILEWIDTH ', '');
this.properties['Tile Height'] = lines[2].replace('TILEHEIGHT ', '');
this.properties['Pixel Size'] = lines[3].replace('PIXELSIZE ', '');
const dataLine = lines[4];
this.properties['Compression Type'] = dataLine.split(',')[2] || 'Uncompressed';
}
this.properties['Bit Depth'] = (parseInt(this.properties['Pixel Size']) * 8 / 4).toString();
this.properties['Has Animation'] = this.zip.getEntry(/keyframes\.xml$/) ? 'Yes' : 'No';
this.properties['ICC Profile'] = this.zip.getEntry(/\.icc$/) ? 'Present' : 'Absent';
const defaultPixel = this.zip.getEntries().find(e => e.entryName.endsWith('.defaultpixel'));
this.properties['Default Pixel'] = defaultPixel ? this.zip.readFile(defaultPixel) : 'N/A';
}
printProperties() {
for (const [k, v] of Object.entries(this.properties)) {
console.log(`${k}: ${v}`);
}
}
write(newFilepath) {
// Example: modify and write new file
const buffer = this.zip.toBuffer();
const newZip = new AdmZip(buffer);
// Modify maindoc.xml example
let maindoc = newZip.readAsText('maindoc.xml');
const parser = new DOMParser();
const xml = parser.parseFromString(maindoc, 'application/xml');
const image = xml.getElementsByTagName('IMAGE')[0];
image.setAttribute('width', (parseInt(image.getAttribute('width')) + 10).toString());
newZip.updateFile('maindoc.xml', Buffer.from(xml.toString()));
newZip.writeZip(newFilepath);
}
close() {
// No close needed for AdmZip
}
}
// Usage example
const handler = new KraHandler('example.kra');
handler.open();
handler.decodeRead();
handler.printProperties();
handler.write('modified.kra');
7. C Class for .KRA File
This is C++ (since "class" implies OO; uses minizip for ZIP, libxml2 for XML; assume installed).
#include <iostream>
#include <string>
#include <map>
#include <zip.h>
#include <libxml/parser.h>
class KraHandler {
private:
std::string filepath;
zip_t* zip;
std::map<std::string, std::string> properties;
public:
KraHandler(const std::string& fp) : filepath(fp), zip(nullptr) {}
~KraHandler() { close(); }
void open() {
int err = 0;
zip = zip_open(filepath.c_str(), 0, &err);
if (!zip) std::cerr << "Failed to open ZIP" << std::endl;
}
void decodeRead() {
if (!zip) open();
// Read maindoc.xml
zip_file_t* file = zip_fopen(zip, "maindoc.xml", 0);
zip_stat_t stat;
zip_stat(zip, "maindoc.xml", 0, &stat);
char* buf = new char[stat.size + 1];
zip_fread(file, buf, stat.size);
buf[stat.size] = '\0';
xmlDocPtr doc = xmlParseMemory(buf, stat.size);
xmlNodePtr root = xmlDocGetRootElement(doc);
xmlNodePtr image = nullptr;
for (xmlNodePtr node = root->children; node; node = node->next) {
if (xmlStrcmp(node->name, (const xmlChar*)"IMAGE") == 0) {
image = node;
break;
}
}
if (image) {
properties["Document Width"] = (char*)xmlGetProp(image, (const xmlChar*)"width");
properties["Document Height"] = (char*)xmlGetProp(image, (const xmlChar*)"height");
properties["Color Space Name"] = (char*)xmlGetProp(image, (const xmlChar*)"colorspacename");
int layerCount = 0;
for (xmlNodePtr node = root; node; node = node->next) if (xmlStrcmp(node->name, (const xmlChar*)"layer") == 0) layerCount++;
properties["Number of Layers"] = std::to_string(layerCount);
}
xmlFreeDoc(doc);
zip_fclose(file);
delete[] buf;
// Find first layer file
const char* layerName = nullptr;
int num = zip_get_num_entries(zip, 0);
for (int i = 0; i < num; i++) {
const char* name = zip_get_name(zip, i, 0);
if (std::string(name).find("/layers/layer") != std::string::npos && std::string(name).find(".") == std::string::npos) {
layerName = name;
break;
}
}
if (layerName) {
file = zip_fopen(zip, layerName, 0);
zip_stat(zip, layerName, 0, &stat);
buf = new char[stat.size + 1];
zip_fread(file, buf, stat.size);
buf[stat.size] = '\0';
std::string data(buf);
size_t pos = 0;
std::string line = data.substr(0, data.find('\n'));
properties["Version"] = line.substr(line.find(' ') + 1);
pos = data.find('\n') + 1;
line = data.substr(pos, data.find('\n', pos) - pos);
properties["Tile Width"] = line.substr(line.find(' ') + 1);
pos = data.find('\n', pos) + 1;
line = data.substr(pos, data.find('\n', pos) - pos);
properties["Tile Height"] = line.substr(line.find(' ') + 1);
pos = data.find('\n', pos) + 1;
line = data.substr(pos, data.find('\n', pos) - pos);
properties["Pixel Size"] = line.substr(line.find(' ') + 1);
pos = data.find('\n', pos) + 1;
line = data.substr(pos, data.find('\n', pos) - pos);
size_t comma2 = line.find(',', line.find(',') + 1);
size_t comma3 = line.find(',', comma2 + 1);
properties["Compression Type"] = line.substr(comma2 + 1, comma3 - comma2 - 1);
zip_fclose(file);
delete[] buf;
}
properties["Bit Depth"] = std::to_string(std::stoi(properties["Pixel Size"]) * 8 / 4);
properties["Has Animation"] = (zip_name_locate(zip, "keyframes.xml", 0) >= 0) ? "Yes" : "No";
properties["ICC Profile"] = (zip_name_locate(zip, ".icc", ZIP_FL_ENC_GUESS | ZIP_FL_NOCASE) >= 0) ? "Present" : "Absent";
// Default pixel
const char* defPixelName = nullptr;
for (int i = 0; i < num; i++) {
const char* name = zip_get_name(zip, i, 0);
if (std::string(name).ends_with(".defaultpixel")) {
defPixelName = name;
break;
}
}
if (defPixelName) {
file = zip_fopen(zip, defPixelName, 0);
zip_stat(zip, defPixelName, 0, &stat);
buf = new char[stat.size];
zip_fread(file, buf, stat.size);
properties["Default Pixel"] = std::string(buf, stat.size); // Binary, so string rep
zip_fclose(file);
delete[] buf;
} else {
properties["Default Pixel"] = "N/A";
}
}
void printProperties() {
for (const auto& p : properties) {
std::cout << p.first << ": " << p.second << std::endl;
}
}
void write(const std::string& newFilepath) {
// Simple copy and modify maindoc.xml
zip_t* newZip = zip_open(newFilepath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, nullptr);
int num = zip_get_num_entries(zip, 0);
for (int i = 0; i < num; i++) {
const char* name = zip_get_name(zip, i, 0);
zip_file_t* file = zip_fopen_index(zip, i, 0);
zip_stat_t stat;
zip_stat_index(zip, i, 0, &stat);
char* buf = new char[stat.size];
zip_fread(file, buf, stat.size);
zip_file_add(newZip, name, zip_buffer_create(buf, stat.size), ZIP_FL_OVERWRITE);
zip_fclose(file);
// Modify if maindoc.xml
if (std::string(name) == "maindoc.xml") {
xmlDocPtr doc = xmlParseMemory(buf, stat.size);
xmlNodePtr image = nullptr;
xmlNodePtr root = xmlDocGetRootElement(doc);
for (xmlNodePtr node = root->children; node; node = node->next) {
if (xmlStrcmp(node->name, (const xmlChar*)"IMAGE") == 0) {
image = node;
break;
}
}
if (image) {
xmlChar* width = xmlGetProp(image, (const xmlChar*)"width");
int newWidth = std::stoi((char*)width) + 10;
xmlSetProp(image, (const xmlChar*)"width", (const xmlChar*)std::to_string(newWidth).c_str());
xmlFree(width);
}
xmlChar* xmlBuf;
int size;
xmlDocDumpMemory(doc, &xmlBuf, &size);
zip_file_replace(newZip, zip_name_locate(newZip, name, 0), zip_buffer_create((char*)xmlBuf, size), 0);
xmlFreeDoc(doc);
}
delete[] buf;
}
zip_close(newZip);
}
void close() {
if (zip) zip_close(zip);
}
};
int main() {
KraHandler handler("example.kra");
handler.open();
handler.decodeRead();
handler.printProperties();
handler.write("modified.kra");
return 0;
}