diff --git a/toonz/sources/image/CMakeLists.txt b/toonz/sources/image/CMakeLists.txt index b610a1d..514ba0c 100644 --- a/toonz/sources/image/CMakeLists.txt +++ b/toonz/sources/image/CMakeLists.txt @@ -24,6 +24,7 @@ set(HEADERS ffmpeg/tiio_webm.h ffmpeg/tiio_mp4.h ffmpeg/tiio_ffmpeg.h + sprite/tiio_sprite.h mesh/tiio_mesh.h ) @@ -55,6 +56,7 @@ set(SOURCES ffmpeg/tiio_webm.cpp ffmpeg/tiio_mp4.cpp ffmpeg/tiio_ffmpeg.cpp + sprite/tiio_sprite.cpp mesh/tiio_mesh.cpp ) diff --git a/toonz/sources/image/sprite/tiio_sprite.cpp b/toonz/sources/image/sprite/tiio_sprite.cpp new file mode 100644 index 0000000..6f839de --- /dev/null +++ b/toonz/sources/image/sprite/tiio_sprite.cpp @@ -0,0 +1,281 @@ + +#include "tiio_sprite.h" +#include "../toonz/tapp.h" +#include "tsystem.h" +#include "tsound.h" + +#include +#include +#include +#include +#include +#include +#include "toonz/preferences.h" +#include "toonz/toonzfolders.h" + +#include "trasterimage.h" +#include "timageinfo.h" + +//=========================================================== +// +// TImageWriterSprite +// +//=========================================================== + +class TImageWriterSprite : public TImageWriter { +public: + int m_frameIndex; + + TImageWriterSprite(const TFilePath &path, int frameIndex, + TLevelWriterSprite *lwg) + : TImageWriter(path), m_frameIndex(frameIndex), m_lwg(lwg) { + m_lwg->addRef(); + } + ~TImageWriterSprite() { m_lwg->release(); } + + bool is64bitOutputSupported() override { return false; } + void save(const TImageP &img) override { m_lwg->save(img, m_frameIndex); } + +private: + TLevelWriterSprite *m_lwg; +}; + +//=========================================================== +// +// TLevelWriterSprite; +// +//=========================================================== + +TLevelWriterSprite::TLevelWriterSprite(const TFilePath &path, + TPropertyGroup *winfo) + : TLevelWriter(path, winfo) { + if (!m_properties) m_properties = new Tiio::SpriteWriterProperties(); + std::string scale = m_properties->getProperty("Scale")->getValueAsString(); + m_scale = QString::fromStdString(scale).toInt(); + std::string topPadding = + m_properties->getProperty("Top Padding")->getValueAsString(); + m_topPadding = QString::fromStdString(topPadding).toInt(); + std::string bottomPadding = + m_properties->getProperty("Bottom Padding")->getValueAsString(); + m_bottomPadding = QString::fromStdString(bottomPadding).toInt(); + std::string leftPadding = + m_properties->getProperty("Left Padding")->getValueAsString(); + m_leftPadding = QString::fromStdString(leftPadding).toInt(); + std::string rightPadding = + m_properties->getProperty("Right Padding")->getValueAsString(); + m_rightPadding = QString::fromStdString(rightPadding).toInt(); + m_format = QString::fromStdWString( + ((TEnumProperty *)(m_properties->getProperty("Format")))->getValue()); + + if (TSystem::doesExistFileOrLevel(m_path)) TSystem::deleteFile(m_path); +} + +//----------------------------------------------------------- + +TLevelWriterSprite::~TLevelWriterSprite() { + int finalWidth = m_right - m_left + 1; + int finalHeight = m_bottom - m_top + 1; + int resizedWidth = finalWidth * m_scale / 100; + int resizedHeight = finalHeight * m_scale / 100; + for (QImage *image : m_images) { + QImage copy = image->copy(m_left, m_top, finalWidth, finalHeight); + if (m_scale != 100) { + int width = (copy.width() * m_scale) / 100; + int height = (copy.height() * m_scale) / 100; + copy = copy.scaled(width, height); + } + m_imagesResized.push_back(copy); + } + for (QImage *image : m_images) { + delete image; + } + m_images.clear(); + int horizDim = 1; + int vertDim = 1; + int vertPadding = m_topPadding + m_bottomPadding; + int horizPadding = m_leftPadding + m_rightPadding; + int totalVertPadding = 0; + int totalHorizPadding = 0; + int spriteSheetWidth; + int spriteSheetHeight; + if (m_format == "Grid") { + while (horizDim * horizDim < m_imagesResized.size()) horizDim++; + totalHorizPadding = horizDim * horizPadding; + spriteSheetWidth = horizDim * resizedWidth + totalHorizPadding; + vertDim = horizDim; + if (vertDim * vertDim - vertDim >= m_imagesResized.size()) { + vertDim = vertDim - 1; + totalVertPadding = vertDim * vertPadding; + } + spriteSheetHeight = vertDim * resizedHeight + totalVertPadding; + } else if (m_format == "Vertical") { + spriteSheetWidth = resizedWidth + horizPadding; + spriteSheetHeight = m_imagesResized.size() * (resizedHeight + vertPadding); + vertDim = m_imagesResized.size(); + } else if (m_format == "Horizontal") { + spriteSheetWidth = m_imagesResized.size() * (resizedWidth + horizPadding); + spriteSheetHeight = resizedHeight + vertPadding; + ; + horizDim = m_imagesResized.size(); + } + + QImage spriteSheet = + QImage(spriteSheetWidth, spriteSheetHeight, QImage::Format_ARGB32); + spriteSheet.fill(qRgba(0, 0, 0, 0)); + QPainter painter; + painter.begin(&spriteSheet); + int row = 0; + int column = 0; + int rowPadding; + int columnPadding; + int currentImage = 0; + while (row < vertDim) { + while (column < horizDim) { + rowPadding = m_topPadding; + columnPadding = m_leftPadding; + rowPadding += row * vertPadding; + columnPadding += column * horizPadding; + painter.drawImage(column * resizedWidth + columnPadding, + row * resizedHeight + rowPadding, + m_imagesResized[currentImage]); + currentImage++; + column++; + if (currentImage >= m_imagesResized.size()) break; + } + column = 0; + row++; + if (currentImage >= m_imagesResized.size()) break; + } + painter.end(); + QString path = m_path.getQString(); + path = path.replace(".spritesheet", ".png"); + spriteSheet.save(path, "PNG", -1); + + path = path.replace(".png", ".txt"); + QFile file(path); + file.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream out(&file); + out << "Total Images: " << m_imagesResized.size() << "\n"; + out << "Individual Image Width: " << resizedWidth << "\n"; + out << "Individual Image Height: " << resizedHeight << "\n"; + out << "Individual Image Width with Padding: " << resizedWidth + horizPadding + << "\n"; + out << "Individual Image Height with Padding: " << resizedHeight + vertPadding + << "\n"; + out << "Images Across: " << horizDim << "\n"; + out << "Images Down : " << vertDim << "\n"; + out << "Top Padding: " << m_topPadding << "\n"; + out << "Bottom Padding: " << m_bottomPadding << "\n"; + out << "Left Padding: " << m_leftPadding << "\n"; + out << "Right Padding: " << m_rightPadding << "\n"; + out << "Horizontal Space Between Images: " << horizPadding << "\n"; + out << "Vertical Space Between Images: " << vertPadding << "\n"; + + file.close(); + m_imagesResized.clear(); +} + +//----------------------------------------------------------- + +TImageWriterP TLevelWriterSprite::getFrameWriter(TFrameId fid) { + if (fid.getLetter() != 0) return TImageWriterP(0); + int index = fid.getNumber(); + TImageWriterSprite *iwg = new TImageWriterSprite(m_path, index, this); + return TImageWriterP(iwg); +} + +//----------------------------------------------------------- +void TLevelWriterSprite::setFrameRate(double fps) {} + +void TLevelWriterSprite::saveSoundTrack(TSoundTrack *st) {} + +//----------------------------------------------------------- + +void TLevelWriterSprite::save(const TImageP &img, int frameIndex) { + TRasterImageP tempImage(img); + TRasterImage *image = (TRasterImage *)tempImage->cloneImage(); + + m_lx = image->getRaster()->getLx(); + m_ly = image->getRaster()->getLy(); + int m_bpp = image->getRaster()->getPixelSize(); + int totalBytes = m_lx * m_ly * m_bpp; + image->getRaster()->yMirror(); + + // lock raster to get data + image->getRaster()->lock(); + void *buffin = image->getRaster()->getRawData(); + assert(buffin); + void *buffer = malloc(totalBytes); + memcpy(buffer, buffin, totalBytes); + + image->getRaster()->unlock(); + + // create QImage save format + QString m_intermediateFormat = "png"; + QByteArray ba = m_intermediateFormat.toUpper().toLatin1(); + const char *format = ba.data(); + + QImage *qi = new QImage((uint8_t *)buffer, m_lx, m_ly, QImage::Format_ARGB32); + + int l = qi->width(), r = 0, t = qi->height(), b = 0; + for (int y = 0; y < qi->height(); ++y) { + QRgb *row = (QRgb *)qi->scanLine(y); + bool rowFilled = false; + for (int x = 0; x < qi->width(); ++x) { + if (qAlpha(row[x])) { + rowFilled = true; + r = std::max(r, x); + if (l > x) { + l = x; + x = r; + } + } + } + if (rowFilled) { + t = std::min(t, y); + b = y; + } + } + if (m_firstPass) { + m_firstPass = false; + m_left = l; + m_right = r; + m_top = t; + m_bottom = b; + } else { + if (l < m_left) m_left = l; + if (r > m_right) m_right = r; + if (t < m_top) m_top = t; + if (b > m_bottom) m_bottom = b; + } + QImage *newQi = new QImage(m_lx, m_ly, QImage::Format_ARGB32); + newQi->fill(qRgba(0, 0, 0, 0)); + QPainter painter(newQi); + painter.drawImage(QPoint(0, 0), *qi); + m_images.push_back(newQi); + delete image; + delete qi; + free(buffer); +} + +Tiio::SpriteWriterProperties::SpriteWriterProperties() + : m_topPadding("Top Padding", 0, 100, 0) + , m_bottomPadding("Bottom Padding", 0, 100, 0) + , m_leftPadding("Left Padding", 0, 100, 0) + , m_rightPadding("Right Padding", 0, 100, 0) + , m_scale("Scale", 1, 100, 100) + , m_format("Format") { + m_format.addValue(L"Grid"); + m_format.addValue(L"Vertical"); + m_format.addValue(L"Horizontal"); + m_format.setValue(L"Grid"); + bind(m_format); + bind(m_topPadding); + bind(m_bottomPadding); + bind(m_leftPadding); + bind(m_rightPadding); + bind(m_scale); +} + +// Tiio::Reader* Tiio::makeSpriteReader(){ return nullptr; } +// Tiio::Writer* Tiio::makeSpriteWriter(){ return nullptr; } \ No newline at end of file diff --git a/toonz/sources/image/sprite/tiio_sprite.h b/toonz/sources/image/sprite/tiio_sprite.h new file mode 100644 index 0000000..623363e --- /dev/null +++ b/toonz/sources/image/sprite/tiio_sprite.h @@ -0,0 +1,63 @@ +#pragma once + +#ifndef TTIO_SPRITE_INCLUDED +#define TTIO_SPRITE_INCLUDED + +#include "tproperty.h" +#include "tlevel_io.h" +#include "trasterimage.h" +#include +#include +#include +//=========================================================== +// +// TLevelWriterSprite +// +//=========================================================== + +class TLevelWriterSprite : public TLevelWriter { +public: + TLevelWriterSprite(const TFilePath &path, TPropertyGroup *winfo); + ~TLevelWriterSprite(); + void setFrameRate(double fps); + + TImageWriterP getFrameWriter(TFrameId fid) override; + void save(const TImageP &image, int frameIndex); + + void saveSoundTrack(TSoundTrack *st); + + static TLevelWriter *create(const TFilePath &path, TPropertyGroup *winfo) { + return new TLevelWriterSprite(path, winfo); + } + +private: + int m_lx, m_ly; + int m_scale; + int m_topPadding, m_bottomPadding, m_leftPadding, m_rightPadding; + int m_left = 0, m_right = 0, m_top = 0, m_bottom = 0; + std::vector m_images; + std::vector m_imagesResized; + bool m_firstPass = true; + QString m_format; + // void *m_buffer; +}; + +//=========================================================================== + +namespace Tiio { + +//=========================================================================== + +class SpriteWriterProperties : public TPropertyGroup { +public: + TEnumProperty m_format; + TIntProperty m_topPadding, m_bottomPadding, m_leftPadding, m_rightPadding; + TIntProperty m_scale; + SpriteWriterProperties(); +}; + +//=========================================================================== + +} // namespace + +#endif diff --git a/toonz/sources/image/tiio.cpp b/toonz/sources/image/tiio.cpp index 9f3ae19..dfbd74d 100644 --- a/toonz/sources/image/tiio.cpp +++ b/toonz/sources/image/tiio.cpp @@ -76,6 +76,7 @@ #include "./ffmpeg/tiio_webm.h" #include "./ffmpeg/tiio_mp4.h" #include "./mesh/tiio_mesh.h" +#include "./sprite/tiio_sprite.h" //------------------------------------------------------------------- @@ -162,6 +163,11 @@ void initImageIo(bool lightVersion) { TFileType::declare("rgb", TFileType::RASTER_IMAGE); Tiio::defineWriterProperties("rgb", new Tiio::SgiWriterProperties()); + TLevelWriter::define("spritesheet", TLevelWriterSprite::create, true); + TFileType::declare("spritesheet", TFileType::RASTER_LEVEL); + Tiio::defineWriterProperties("spritesheet", + new Tiio::SpriteWriterProperties()); + // ffmpeg #if !defined(_WIN32) || defined(x64) if (Ffmpeg::checkFfmpeg()) { diff --git a/toonz/sources/toonz/outputsettingspopup.cpp b/toonz/sources/toonz/outputsettingspopup.cpp index e8a57b9..705cc87 100644 --- a/toonz/sources/toonz/outputsettingspopup.cpp +++ b/toonz/sources/toonz/outputsettingspopup.cpp @@ -943,7 +943,7 @@ void OutputSettingsPopup::onFormatChanged(const QString &str) { TApp::instance()->getCurrentScene()->setDirtyFlag(true); if (m_presetCombo) m_presetCombo->setCurrentIndex(0); - if (str == "mp4" || str == "gif" || str == "webm") { + if (str == "mp4" || str == "gif" || str == "webm" || str == "spritesheet") { m_threadsComboOm->setDisabled(true); m_threadsComboOm->setCurrentIndex(0); } else { diff --git a/toonz/sources/toonz/rendercommand.cpp b/toonz/sources/toonz/rendercommand.cpp index 5af6af1..3f25c3c 100644 --- a/toonz/sources/toonz/rendercommand.cpp +++ b/toonz/sources/toonz/rendercommand.cpp @@ -172,7 +172,7 @@ public: app->getCurrentXsheet()->getXsheet()->makeSound(prop); // keeps ffmpeg files from being previewed until import is fixed if (m_fp.getType() != "mp4" && m_fp.getType() != "webm" && - m_fp.getType() != "gif") { + m_fp.getType() != "gif" && m_fp.getType() != "spritesheet") { if (outputSettings.getRenderSettings().m_stereoscopic) { assert(!isPreview); ::viewFile(m_fp.withName(m_fp.getName() + "_l"), r0 + 1, r1 + 1,