488 lines
15 KiB
C++
488 lines
15 KiB
C++
|
|
|
|
// TnzCore includes
|
|
#include "tsystem.h"
|
|
#include "trasterimage.h"
|
|
#include "tiio.h"
|
|
#include "timageinfo.h"
|
|
#include "tcontenthistory.h"
|
|
|
|
// TnzLib includes
|
|
#include "toonz/txshleveltypes.h"
|
|
#include "toonz/imagemanager.h"
|
|
#include "toonz/toonzscene.h"
|
|
#include "toonz/levelproperties.h"
|
|
#include "toonz/preferences.h"
|
|
#include "toonz/sceneproperties.h"
|
|
|
|
#include "toonz/levelupdater.h"
|
|
|
|
//*****************************************************************************************
|
|
// Local namespace stuff
|
|
//*****************************************************************************************
|
|
|
|
namespace
|
|
{
|
|
|
|
inline bool supportsRandomAccess(const TFilePath &fp)
|
|
{
|
|
const std::string &type = fp.getType();
|
|
return type == "tlv" || // TLVs do support random access
|
|
//type == "pli" || // PLIs... I thought they would - but no :(
|
|
//type == "mov" || // MOVs are 'on the way' to support it... for now, no
|
|
fp.getDots() == ".."; // Multi-file levels of course do
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void enforceBpp(TPropertyGroup *pg, int bpp, bool upgradeOnly)
|
|
{
|
|
// Most properties have a "Bits Per Pixel" property. Enforce the M there in case.
|
|
TEnumProperty *bppProp = (TEnumProperty *)pg->getProperty("Bits Per Pixel");
|
|
if (bppProp) {
|
|
typedef TEnumProperty::Range Range;
|
|
const Range &range = bppProp->getRange();
|
|
|
|
// Retrieve current index
|
|
int idx = bppProp->getIndex();
|
|
|
|
// Search for a suitable 32-bit or 64-bit value
|
|
int currentBpp = upgradeOnly ? atoi(bppProp->getValueAsString().c_str()) : 0;
|
|
int targetBpp = (std::numeric_limits<int>::max)(), targetIdx = -1;
|
|
|
|
int i, count = (int)range.size();
|
|
for (i = 0; i < count; ++i) {
|
|
int bppEntry = atoi(toString(range[i]).c_str());
|
|
if ((bppEntry % bpp == 0) && currentBpp <= bppEntry && bppEntry < targetBpp)
|
|
targetBpp = bppEntry, targetIdx = i;
|
|
}
|
|
|
|
if (targetIdx >= 0)
|
|
bppProp->setIndex(targetIdx);
|
|
}
|
|
|
|
// Some properties have an "Alpha Channel" TBoolProperty (PNGs, currently). In case, check that.
|
|
if (bpp % 32 == 0) {
|
|
TBoolProperty *alphaProp = (TBoolProperty *)pg->getProperty("Alpha Channel");
|
|
if (alphaProp)
|
|
alphaProp->setValue(true);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
//*****************************************************************************************
|
|
// LevelUpdater implementation
|
|
//*****************************************************************************************
|
|
|
|
LevelUpdater::LevelUpdater()
|
|
: m_pg(0), m_inputLevel(0), m_currIdx(0), m_imageInfo(0), m_usingTemporaryFile(false), m_opened(false)
|
|
{
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
LevelUpdater::LevelUpdater(TXshSimpleLevel *sl)
|
|
: m_pg(0), m_inputLevel(0), m_imageInfo(0), m_currIdx(0), m_opened(false), m_usingTemporaryFile(false)
|
|
{
|
|
open(sl);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
LevelUpdater::LevelUpdater(const TFilePath &fp, TPropertyGroup *lwProperties)
|
|
: m_pg(0), m_inputLevel(0), m_imageInfo(0), m_currIdx(0), m_opened(false), m_usingTemporaryFile(false)
|
|
{
|
|
open(fp, lwProperties);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
LevelUpdater::~LevelUpdater()
|
|
{
|
|
// Please, observe that the try-catch below here is NOT OPTIONAL.
|
|
// IT IS AN ERROR TO THROW INSIDE A DESTRUCTOR. EVER.
|
|
// Doing so damages the stack unwinding process - namely, it interferes
|
|
// with the destruction of OTHER objects going out of scope.
|
|
// C++ does NOT react well to that (ie could terminate() the process).
|
|
|
|
try {
|
|
close();
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::reset()
|
|
{
|
|
m_lw = TLevelWriterP();
|
|
m_lwPath = TFilePath();
|
|
|
|
m_lr = TLevelReaderP();
|
|
m_inputLevel = TLevelP();
|
|
m_sl = TXshSimpleLevelP();
|
|
|
|
delete m_pg;
|
|
m_pg = 0;
|
|
|
|
if (m_imageInfo) {
|
|
delete m_imageInfo->m_properties;
|
|
delete m_imageInfo;
|
|
m_imageInfo = 0;
|
|
}
|
|
|
|
m_fids.clear();
|
|
m_currIdx = 0;
|
|
|
|
m_usingTemporaryFile = false;
|
|
m_opened = false;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::buildSourceInfo(const TFilePath &fp)
|
|
{
|
|
try {
|
|
m_lr = TLevelReaderP(fp);
|
|
assert(m_lr);
|
|
|
|
m_lr->enableRandomAccessRead(true); // Movie files are intended with a constant fps
|
|
// should be made the default... TODO!
|
|
m_inputLevel = m_lr->loadInfo();
|
|
|
|
const TImageInfo *info = m_lr->getImageInfo();
|
|
if (info) {
|
|
m_imageInfo = new TImageInfo(*info); // Clone the info. The originals are owned by the reader.
|
|
if (info->m_properties)
|
|
m_imageInfo->m_properties = info->m_properties->clone(); // Same for these (unfortunately, TImageInfo is currently
|
|
// no more than a struct...)
|
|
}
|
|
} catch (...) {
|
|
// The level exists but could not be read.
|
|
// Allowing write to a surviving temporary in this case...
|
|
|
|
m_lr = TLevelReaderP();
|
|
m_inputLevel = TLevelP(0);
|
|
|
|
if (m_imageInfo) {
|
|
delete m_imageInfo->m_properties;
|
|
delete m_imageInfo;
|
|
m_imageInfo = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::buildProperties(const TFilePath &fp)
|
|
{
|
|
// Ensure that at least the default properties for specified fp.getType() exist.
|
|
m_pg = (m_imageInfo && m_imageInfo->m_properties) ? m_imageInfo->m_properties->clone() : Tiio::makeWriterProperties(fp.getType());
|
|
|
|
if (!m_pg) {
|
|
// If no suitable pg could be found, the extension must be wrong. Reset and throw.
|
|
reset();
|
|
throw TException("Unrecognized file format");
|
|
}
|
|
|
|
assert(m_pg);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::open(const TFilePath &fp, TPropertyGroup *pg)
|
|
{
|
|
assert(!m_lw);
|
|
|
|
// Find out if a corresponding level already exists on disk - in that case, load it
|
|
bool existsLevel = TSystem::doesExistFileOrLevel(fp);
|
|
if (existsLevel)
|
|
buildSourceInfo(fp); // Could be !m_lr if level could not be read
|
|
|
|
// Build Output Properties if needed
|
|
if (pg)
|
|
m_pg = pg->clone();
|
|
else
|
|
buildProperties(fp); // Throws only if not even the default properties
|
|
// could be found - ie, bad file type
|
|
try {
|
|
// Decide whether the update procedure requires a temporary file for appending
|
|
m_usingTemporaryFile = existsLevel && !supportsRandomAccess(fp);
|
|
if (m_usingTemporaryFile) {
|
|
// The level requires a temporary to write frames to. Upon closing, the original level
|
|
// is deleted and the temporary takes its place. Note that m_lw takes ownership of the properties group.
|
|
m_lwPath = getNewTemporaryFilePath(fp);
|
|
m_lw = TLevelWriterP(m_lwPath, m_pg->clone());
|
|
|
|
if (m_inputLevel)
|
|
for (TLevel::Iterator it = m_inputLevel->begin(); it != m_inputLevel->end(); ++it)
|
|
m_fids.push_back(it->first);
|
|
} else {
|
|
m_lr = TLevelReaderP(); // Release the reader. This is necessary since the
|
|
m_lw = TLevelWriterP(fp, m_pg->clone()); // original file itself will be MODIFIED.
|
|
m_lwPath = fp;
|
|
}
|
|
} catch (...) {
|
|
// In this case, TLevelWriterP(..) failed, that object was never contructed,
|
|
// the assignment m_lw never took place. And m_lw == 0.
|
|
|
|
// Reset state and rethrow
|
|
reset();
|
|
throw;
|
|
}
|
|
|
|
// In case the writer saves icons inside the output level (TLV case), set the associated icon size now
|
|
TDimension iconSize = Preferences::instance()->getIconSize();
|
|
assert(iconSize.lx > 0 && iconSize.ly > 0);
|
|
m_lw->setIconSize(iconSize);
|
|
|
|
m_opened = true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::open(TXshSimpleLevel *sl)
|
|
{
|
|
assert(!m_lw);
|
|
|
|
assert(sl && sl->getScene());
|
|
m_sl = sl;
|
|
|
|
const TFilePath &fp = sl->getScene()->decodeFilePath(sl->getPath());
|
|
|
|
// Find out if a corresponding level already exists on disk - in that case, load it
|
|
bool existsLevel = TSystem::doesExistFileOrLevel(fp);
|
|
if (existsLevel)
|
|
buildSourceInfo(fp); // Could be !m_lr if level could not be read
|
|
|
|
// Build Output Properties
|
|
buildProperties(fp); // May throw if not even the default properties could be
|
|
// retrieved
|
|
|
|
// If there was no level on disk, or the level properties require the alpha channel, enforce the
|
|
// bpp accordingly on m_pg.
|
|
LevelProperties *levelProperties = sl->getProperties();
|
|
assert(levelProperties);
|
|
|
|
if (levelProperties->hasAlpha() || !existsLevel) {
|
|
int bpp = levelProperties->hasAlpha() ? tmin(32, levelProperties->getBpp()) : levelProperties->getBpp();
|
|
enforceBpp(m_pg, bpp, existsLevel);
|
|
}
|
|
|
|
// Should sl->getPalette() be enforced on m_lw too? It was not present in the old code...
|
|
|
|
try {
|
|
// Decide whether the update procedure requires a temporary file for appending
|
|
m_usingTemporaryFile = existsLevel && !supportsRandomAccess(fp);
|
|
if (m_usingTemporaryFile) {
|
|
// The level requires a temporary to write frames to. Upon closing, the original level
|
|
// is deleted and the temporary takes its place.
|
|
m_lwPath = getNewTemporaryFilePath(fp);
|
|
m_lw = TLevelWriterP(m_lwPath, m_pg->clone());
|
|
} else {
|
|
m_lr = TLevelReaderP(); // Release the reader
|
|
m_lw = TLevelWriterP(fp, m_pg->clone()); // Open for write the usual way
|
|
m_lwPath = fp;
|
|
}
|
|
} catch (...) {
|
|
// Reset state and rethrow
|
|
reset();
|
|
throw;
|
|
}
|
|
|
|
// Load the frames directly from sl
|
|
sl->getFids(m_fids);
|
|
|
|
// In case the writer saves icons inside the output level (TLV case), set the associated icon size now
|
|
TDimension iconSize = Preferences::instance()->getIconSize();
|
|
assert(iconSize.lx > 0 && iconSize.ly > 0);
|
|
m_lw->setIconSize(iconSize);
|
|
|
|
if (sl->getContentHistory())
|
|
m_lw->setContentHistory(m_sl->getContentHistory()->clone());
|
|
|
|
m_opened = true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
TFilePath LevelUpdater::getNewTemporaryFilePath(const TFilePath &fp)
|
|
{
|
|
TFilePath fp2;
|
|
int count = 1;
|
|
|
|
for (;;) {
|
|
fp2 = fp.withName(fp.getWideName() + L"__" + toWideString(count++));
|
|
if (!TSystem::doesExistFileOrLevel(fp2))
|
|
break;
|
|
}
|
|
|
|
return fp2;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::addFramesTo(int endIdx)
|
|
{
|
|
if (m_sl) {
|
|
// The simple level case can be optimized since some level's images could already be present
|
|
// in memory. Images are accessed through the level itself.
|
|
|
|
for (; m_currIdx < endIdx; ++m_currIdx) {
|
|
TImageP img = m_sl->getFullsampledFrame(m_fids[m_currIdx], ImageManager::dontPutInCache);
|
|
assert(img);
|
|
|
|
if (!img && m_lr) {
|
|
// This should actually never happen. ImageManager should already ensure that img exists.
|
|
// However, as last resort let's just look at the file too...
|
|
img = m_lr->getFrameReader(m_fids[m_currIdx])->load();
|
|
if (img)
|
|
img->setPalette(m_sl->getPalette());
|
|
}
|
|
|
|
if (img)
|
|
m_lw->getFrameWriter(m_fids[m_currIdx])->save(img);
|
|
}
|
|
} else if (m_lr) {
|
|
// Otherwise, just look in the file directly
|
|
for (; m_currIdx < endIdx; ++m_currIdx) {
|
|
TImageP img = m_lr->getFrameReader(m_fids[m_currIdx])->load();
|
|
|
|
if (img)
|
|
m_lw->getFrameWriter(m_fids[m_currIdx])->save(img);
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::update(const TFrameId &fid, const TImageP &img)
|
|
{
|
|
// Resume open for write
|
|
resume();
|
|
|
|
if (!m_usingTemporaryFile) {
|
|
// Plain random access write if supported
|
|
m_lw->getFrameWriter(fid)->save(img);
|
|
return;
|
|
}
|
|
|
|
// Otherwise, we must add every frame preceding fid, and *then* add img.
|
|
// NOTE: This requires that the image sequence is already sorted by fid.
|
|
addFramesTo(std::lower_bound(m_fids.begin() + m_currIdx, m_fids.end(), fid) - m_fids.begin());
|
|
|
|
// Save the passed image. In case it overwrites a frame, erase that from the list too.
|
|
m_lw->getFrameWriter(fid)->save(img);
|
|
if (m_currIdx < int(m_fids.size()) && m_fids[m_currIdx] == fid)
|
|
++m_currIdx;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::close()
|
|
{
|
|
if (!m_opened)
|
|
return;
|
|
|
|
// Resume open for write
|
|
resume();
|
|
|
|
try {
|
|
if (m_usingTemporaryFile) {
|
|
// Add all remaining frames still in m_fids
|
|
addFramesTo((int)m_fids.size());
|
|
|
|
// Currently written level is temporary. It must be renamed to its originally intended path,
|
|
// if it's possible to write there. Now, if it's writable, in particular it should be readable,
|
|
// so m_lr should exist.
|
|
|
|
// If not... well, the file was corrupt or something. Instead than attempting to delete it,
|
|
// we're begin conservative - this means that no data is lost, but unfortunately temporaries
|
|
// might pile up...
|
|
if (m_lr) {
|
|
TFilePath finalPath(m_lr->getFilePath()), tempPath(m_lw->getFilePath());
|
|
|
|
// Release m_lr and m_lw - to be sure that no file is kept open while renaming.
|
|
// NOTE: releasing m_lr and m_lw should not throw anything. As stated before, throwing
|
|
// in destructors is bad. I'm not sure this is actually guaranteed in Toonz, however :(
|
|
m_lr = TLevelReaderP(), m_lw = TLevelWriterP();
|
|
|
|
// Rename the level
|
|
TSystem::removeFileOrLevel_throw(finalPath);
|
|
TSystem::renameFileOrLevel_throw(finalPath, tempPath); // finalPath <- tempPath
|
|
|
|
// If present, add known trailing files
|
|
if (finalPath.getType() == "tlv") {
|
|
// Palette file
|
|
TFilePath finalPalette = finalPath.withType("tpl");
|
|
TFilePath tempPalette = tempPath.withType("tpl");
|
|
|
|
if (TFileStatus(finalPalette).doesExist()) {
|
|
if (TFileStatus(tempPalette).doesExist())
|
|
TSystem::deleteFile(finalPalette);
|
|
TSystem::renameFile(finalPalette, tempPalette);
|
|
}
|
|
|
|
// History file
|
|
TFilePath finalHistory = finalPath.withType("hst");
|
|
TFilePath tempHistory = tempPath.withType("hst");
|
|
|
|
if (TFileStatus(tempHistory).doesExist()) {
|
|
if (TFileStatus(finalHistory).doesExist())
|
|
TSystem::deleteFile(finalHistory);
|
|
TSystem::renameFile(finalHistory, tempHistory);
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: If for some reason m_lr was not present and we were using a temporary file, no
|
|
// renaming takes place. Users could see the __x temporaries and, eventually, rename them manually
|
|
// or see what's wrong with the unwritable file.
|
|
}
|
|
|
|
// Reset the updater's status
|
|
reset();
|
|
} catch (...) {
|
|
// Some temporary object could not be renamed. Or some remaining frame could not be added.
|
|
// Hopefully, it was not about closing m_lr or m_lw.
|
|
|
|
// However, we still intend to reset the updater's status before rethrowing.
|
|
reset();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::flush()
|
|
{
|
|
assert(m_opened);
|
|
if (!m_lw)
|
|
return;
|
|
|
|
// In case the level writer could not be destroyed (bad, should really not throw btw),
|
|
// reset and rethrow
|
|
try {
|
|
m_lw = TLevelWriterP();
|
|
} catch (...) {
|
|
reset();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void LevelUpdater::resume()
|
|
{
|
|
assert(m_opened);
|
|
if (m_lw)
|
|
return;
|
|
|
|
try {
|
|
m_lw = TLevelWriterP(m_lwPath, m_pg->clone());
|
|
} catch (...) {
|
|
reset();
|
|
throw;
|
|
}
|
|
}
|