Commit f04e55f0 authored by Fazekas László's avatar Fazekas László Committed by Dmitry Kazakov
Browse files

CSV animation file format export-import plugin for Krita

This plugin can export and import TVPaint's layered animation
format. The format contains a CSV (comma separated values)
textfile (filename.csv) and a directory (filename.frames)
with png images of the keyframes. TVPaint itself can only
export this format, but I'm working on an importer script
for TVPaint too.

Differential revision: https://phabricator.kde.org/D1068
parent a1be8613
......@@ -14,6 +14,7 @@ endif()
if(PNG_FOUND)
add_subdirectory(png)
add_subdirectory(csv)
endif()
if(OPENEXR_FOUND)
......
add_subdirectory(tests)
include_directories( ${CMAKE_SOURCE_DIR}/krita/image/metadata )
# import
set(kritacsvimport_SOURCES
kis_csv_import.cpp
csv_loader.cpp
csv_read_line.cpp
csv_layer_record.cpp
)
add_library(kritacsvimport MODULE ${kritacsvimport_SOURCES})
target_link_libraries(kritacsvimport kritaui )
install(TARGETS kritacsvimport DESTINATION ${CALLIGRA_PLUGIN_INSTALL_DIR})
# export
set(kritacsvexport_SOURCES
kis_csv_export.cpp
csv_saver.cpp
csv_layer_record.cpp
)
add_library(kritacsvexport MODULE ${kritacsvexport_SOURCES})
target_link_libraries(kritacsvexport kritaui )
install(TARGETS kritacsvexport DESTINATION ${CALLIGRA_PLUGIN_INSTALL_DIR})
install( PROGRAMS krita_csv.desktop DESTINATION ${XDG_APPS_INSTALL_DIR})
/*
* Copyright (c) 2016 Laszlo Fazekas <mneko@freemail.hu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "csv_layer_record.h"
CSVLayerRecord::CSVLayerRecord()
: channel(NULL)
, layer(NULL)
, last("")
, frame(0)
{
}
CSVLayerRecord::~CSVLayerRecord()
{
}
/*
* Copyright (c) 2016 Laszlo Fazekas <mneko@freemail.hu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifndef CSV_LAYER_RECORD_H_
#define CSV_LAYER_RECORD_H_
#include <QString>
#include "kis_raster_keyframe_channel.h"
class CSVLayerRecord
{
public:
CSVLayerRecord();
virtual ~CSVLayerRecord();
QString name;
QString blending;
float density;
int visible;
KisPaintLayer* layer;
KisRasterKeyframeChannel *channel;
QString last;
int frame;
};
#endif
/*
* Copyright (c) 2016 Laszlo Fazekas <mneko@freemail.hu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "csv_loader.h"
#include <QDebug>
#include <QApplication>
#include <QFile>
#include <QVector>
#include <QIODevice>
#include <QProgressDialog>
#include <KisPart.h>
#include <KisDocument.h>
#include <KoColorSpace.h>
#include <KoColorSpaceRegistry.h>
#include <KoColorModelStandardIds.h>
#include <kis_debug.h>
#include <kis_image.h>
#include <kis_paint_layer.h>
#include <kis_raster_keyframe_channel.h>
#include <kis_image_animation_interface.h>
#include <kis_time_range.h>
#include "csv_read_line.h"
#include "csv_layer_record.h"
CSVLoader::CSVLoader(KisDocument *doc)
: m_image(0)
, m_doc(doc)
, m_stop(false)
{
}
CSVLoader::~CSVLoader()
{
}
KisImageBuilder_Result CSVLoader::decode(const QUrl &uri, const QString &filename)
{
QString field;
int idx;
int frame = 0;
QString projName;
int width = 0;
int height = 0;
int frameCount = 1;
float framerate = 24.0;
float pixelRatio = 1.0;
int projNameIdx = -1;
int widthIdx = -1;
int heightIdx = -1;
int frameCountIdx = -1;
int framerateIdx = -1;
int pixelRatioIdx = -1;
QVector<CSVLayerRecord*> layers;
// open the csv file
QFile f(uri.toLocalFile());
if (!f.exists())
return KisImageBuilder_RESULT_NOT_EXIST;
if (!f.open(QIODevice::ReadOnly))
return KisImageBuilder_RESULT_NOT_EXIST;
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
QString path = filename;
if (path.right(4).toUpper() == ".CSV")
path = path.left(path.size() - 4);
//according to the QT docs, the slash is a universal directory separator
path.append(".frames/");
KisImageBuilder_Result retval = KisImageBuilder_RESULT_OK;
dbgFile << "pos:" << f.pos();
CSVReadLine readLine;
QScopedPointer<KisDocument> importDoc(KisPart::instance()->createDocument());
importDoc->setAutoSave(0);
importDoc->setSaveInBatchMode(true);
QProgressDialog progress(i18n("Importing CSV..."), i18n("Cancel"),
0, 0, KisPart::instance()->currentMainwindow(), 0);
int step = 0;
do {
qApp->processEvents();
if (progress.wasCanceled()) {
retval = KisImageBuilder_RESULT_FAILURE;
break;
}
if ((idx = readLine.nextLine(&f)) <= 0) {
if ((idx < 0) ||(step < 5))
retval = KisImageBuilder_RESULT_FAILURE;
break;
}
field = readLine.nextField(); //first field of the line
if (field.isNull()) continue; //empty row
switch (step) {
case 0 : //skip first row
step = 1;
break;
case 1 : //scene header names
step = 2;
for (idx = 0; !field.isNull(); idx++) {
if (field == "Project Name") {
projNameIdx = idx;
} else if (field == "Width") {
widthIdx = idx;
} else if (field == "Height") {
heightIdx = idx;
} else if (field == "Frame Count") {
frameCountIdx = idx;
} else if (field == "Frame Rate") {
framerateIdx = idx;
} else if (field == "Pixel Aspect Ratio") {
pixelRatioIdx = idx;
}
field= readLine.nextField();
}
break;
case 2 : //scene header values
step= 3;
for (idx= 0; !field.isNull(); idx++) {
if (idx == projNameIdx) {
projName = field;
} else if (idx == widthIdx) {
width = field.toInt();
} else if (idx == heightIdx) {
height = field.toInt();
} else if (idx == frameCountIdx) {
frameCount = field.toInt();
if (frameCount < 1) frameCount= 1;
} else if (idx == framerateIdx) {
framerate = field.toFloat();
} else if (idx == pixelRatioIdx) {
pixelRatio = field.toFloat();
}
field= readLine.nextField();
}
if ((width < 1) || (height < 1)) {
retval = KisImageBuilder_RESULT_FAILURE;
break;
}
progress.setWindowModality(Qt::ApplicationModal);
progress.setMinimumDuration(1000);
progress.setMaximum(frameCount);
progress.setValue(0);
retval = createNewImage(width, height, pixelRatio, projName.isNull() ? filename : projName);
break;
case 3 : //create level headers
if (field[0] != '#') break;
for (; !(field = readLine.nextField()).isNull(); ) {
CSVLayerRecord* layerRecord = new CSVLayerRecord();
layers.append(layerRecord);
}
readLine.rewind();
field = readLine.nextField();
step = 4;
//no break!
case 4 : //level header
if (field == "#Layers") {
//layer name
for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++)
layers.at(idx)->name = field;
break;
}
if (field == "#Density") {
//layer opacity
for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++)
layers.at(idx)->density = field.toFloat();
break;
}
if (field == "#Blending") {
//layer blending mode
for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++)
layers.at(idx)->blending = field;
break;
}
if (field == "#Visible") {
//layer visibility
for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++)
layers.at(idx)->visible = field.toInt();
break;
}
if ((field.size() < 2) || (field[0] != '#') || !field[1].isDigit()) break;
step = 5;
//no break!
case 5 : //frames
progress.setValue(frame);
if ((field.size() < 2) || (field[0] != '#') || !field[1].isDigit()) break;
for (idx = 0; !(field = readLine.nextField()).isNull() && (idx < layers.size()); idx++) {
CSVLayerRecord* layer = layers.at(idx);
if (layer->last != field) {
retval = setLayer(layer, importDoc.data(), path);
layer->last = field;
layer->frame = frame;
}
}
frame++;
break;
}
} while (retval == KisImageBuilder_RESULT_OK);
//finish the layers
if (retval == KisImageBuilder_RESULT_OK) {
if (m_image) {
KisImageAnimationInterface *animation = m_image->animationInterface();
if (frame > frameCount)
frameCount = frame;
animation->setFullClipRange(KisTimeRange::fromTime(0,frameCount - 1));
animation->setFramerate((int)framerate);
}
for (idx = 0; idx < layers.size(); idx++) {
CSVLayerRecord* layer = layers.at(idx);
//empty layers without any pictures are dropped
if ((layer->frame > 0) || !layer->last.isEmpty()) {
retval = setLayer(layer, importDoc.data(), path);
if (retval != KisImageBuilder_RESULT_OK)
break;
}
}
}
if (m_image) {
//insert the existing layers by the right order
for (idx = layers.size() - 1; idx >= 0; idx--) {
CSVLayerRecord* layer = layers.at(idx);
if (layer->layer) {
m_image->addNode(layer->layer, m_image->root());
}
}
m_image->unlock();
}
qDeleteAll(layers);
f.close();
QApplication::restoreOverrideCursor();
return retval;
}
QString CSVLoader::convertBlending(const QString &blending)
{
if (blending == "Color") return COMPOSITE_OVER;
if (blending == "Behind") return COMPOSITE_BEHIND;
if (blending == "Erase") return COMPOSITE_ERASE;
// "Shade"
if (blending == "Light") return COMPOSITE_LINEAR_LIGHT;
if (blending == "Colorize") return COMPOSITE_COLORIZE;
if (blending == "Hue") return COMPOSITE_HUE;
if (blending == "Add") return COMPOSITE_ADD;
if (blending == "Sub") return COMPOSITE_INVERSE_SUBTRACT;
if (blending == "Multiply") return COMPOSITE_MULT;
if (blending == "Screen") return COMPOSITE_SCREEN;
// "Replace"
// "Subtitute"
if (blending == "Difference") return COMPOSITE_DIFF;
if (blending == "Divide") return COMPOSITE_DIVIDE;
if (blending == "Overlay") return COMPOSITE_OVERLAY;
if (blending == "Light2") return COMPOSITE_DODGE;
if (blending == "Shade2") return COMPOSITE_BURN;
if (blending == "HardLight") return COMPOSITE_HARD_LIGHT;
if (blending == "SoftLight") return COMPOSITE_SOFT_LIGHT_PHOTOSHOP;
if (blending == "GrainExtract") return COMPOSITE_GRAIN_EXTRACT;
if (blending == "GrainMerge") return COMPOSITE_GRAIN_MERGE;
if (blending == "Sub2") return COMPOSITE_SUBTRACT;
if (blending == "Darken") return COMPOSITE_DARKEN;
if (blending == "Lighten") return COMPOSITE_LIGHTEN;
if (blending == "Saturation") return COMPOSITE_SATURATION;
return COMPOSITE_OVER;
}
KisImageBuilder_Result CSVLoader::setLayer(CSVLayerRecord* layer, KisDocument *importDoc, const QString &path)
{
bool result = true;
if (layer->channel == NULL) {
//create a new document layer
float opacity = layer->density;
if (opacity > 1.0)
opacity = 1.0;
else if (opacity < 0.0)
opacity = 0.0;
const KoColorSpace* cs = m_image->colorSpace();
const QString layerName = (layer->name).isEmpty() ? m_image->nextLayerName() : layer->name;
KisPaintLayer* paintLayer = new KisPaintLayer(m_image, layerName,
(quint8)(opacity * OPACITY_OPAQUE_U8), cs);
paintLayer->setCompositeOpId(convertBlending(layer->blending));
paintLayer->setVisible(layer->visible);
paintLayer->enableAnimation();
layer->layer = paintLayer;
layer->channel = qobject_cast<KisRasterKeyframeChannel*>
(paintLayer->getKeyframeChannel(KisKeyframeChannel::Content.id()));
}
layer->channel->addKeyframe(layer->frame);
if (!layer->last.isEmpty()) {
//png image
QString filename = path;
filename.append(layer->last);
result = importDoc->openUrl(QUrl::fromLocalFile(filename),
KisDocument::OPEN_URL_FLAG_DO_NOT_ADD_TO_RECENT_FILES);
if (result)
layer->channel->importFrame(layer->frame, importDoc->image()->projection(), NULL);
}
return (result) ? KisImageBuilder_RESULT_OK : KisImageBuilder_RESULT_FAILURE;
}
KisImageBuilder_Result CSVLoader::createNewImage(int width, int height, float ratio, const QString &name)
{
//the CSV is RGBA 8bits, sRGB
if (!m_image) {
const KoColorSpace* cs = KoColorSpaceRegistry::instance()->colorSpace(
RGBAColorModelID.id(), Integer8BitsColorDepthID.id(), 0);
if (cs) m_image = new KisImage(m_doc->createUndoStore(), width, height, cs, name);
if (!m_image) return KisImageBuilder_RESULT_FAILURE;
m_image->setResolution(ratio, 1.0);
m_image->lock();
}
return KisImageBuilder_RESULT_OK;
}
KisImageBuilder_Result CSVLoader::buildAnimation(const QUrl &uri,const QString &filename)
{
if (uri.isEmpty())
return KisImageBuilder_RESULT_NO_URI;
if (!uri.isLocalFile())
return KisImageBuilder_RESULT_NOT_EXIST;
return decode(uri, filename);
}
KisImageWSP CSVLoader::image()
{
return m_image;
}
/*
* Copyright (c) 2016 Laszlo Fazekas <mneko@freemail.hu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifndef CSV_LOADER_H_
#define CSV_LOADER_H_
#include <QObject>
#include <QUrl>
#include "kis_image.h"
#include "kritaui_export.h"
class KisDocument;
#include "csv_layer_record.h"
/**
* Image import/export plugins can use these results to report about success or failure.
*/
enum KisImageBuilder_Result {
KisImageBuilder_RESULT_FAILURE = -400,
KisImageBuilder_RESULT_NOT_EXIST = -300,
KisImageBuilder_RESULT_NOT_LOCAL = -200,
KisImageBuilder_RESULT_BAD_FETCH = -100,
KisImageBuilder_RESULT_INVALID_ARG = -50,
KisImageBuilder_RESULT_OK = 0,
KisImageBuilder_RESULT_PROGRESS = 1,
KisImageBuilder_RESULT_EMPTY = 100,
KisImageBuilder_RESULT_BUSY = 150,
KisImageBuilder_RESULT_NO_URI = 200,
KisImageBuilder_RESULT_UNSUPPORTED = 300,
KisImageBuilder_RESULT_INTR = 400,
KisImageBuilder_RESULT_PATH = 500,
KisImageBuilder_RESULT_UNSUPPORTED_COLORSPACE = 600
};
class CSVLoader : public QObject {
Q_OBJECT
public:
CSVLoader(KisDocument* doc);
virtual ~CSVLoader();
KisImageBuilder_Result buildAnimation(const QUrl &, const QString &);
KisImageWSP image();
private:
KisImageBuilder_Result decode(const QUrl &, const QString &);
KisImageBuilder_Result setLayer(CSVLayerRecord* , KisDocument* ,const QString &);
KisImageBuilder_Result createNewImage(int, int, float, const QString &);
QString convertBlending(const QString &);