// 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();
if (type == "pli") return false;
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 ? std::stoi(bppProp->getValueAsString()) : 0;
int targetBpp = (std::numeric_limits<int>::max)(), targetIdx = -1;
int i, count = (int)range.size();
for (i = 0; i < count; ++i) {
int bppEntry = std::stoi(range[i]);
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,
const TFrameId &tmplFId)
: m_pg(0)
, m_inputLevel(0)
, m_imageInfo(0)
, m_currIdx(0)
, m_opened(false)
, m_usingTemporaryFile(false) {
open(fp, lwProperties, tmplFId);
}
//-----------------------------------------------------------------------------
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,
const TFrameId &tmplFId) {
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 = m_lw->getFilePath();
}
} catch (...) {
// In this case, TLevelWriterP(..) failed, that object was never
// constructed,
// 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);
// set the frame format template (to be used in
// TLevelWriter::getFrameWriter())
if (!tmplFId.isNoFrame()) m_lw->setFrameFormatTemplateFId(tmplFId);
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()
? std::min(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 (;;) {
// changed the temporary name as the previous naming (like
// "filename__1.png") had been misteken as sequential images
fp2 = fp.withName(fp.getWideName() + L"_ottmp" + std::to_wstring(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();
// A temp file didn't get created. What happened? Force failure!
if (!TFileStatus(tempPath).doesExist())
throw TSystemException(tempPath, "cant find!");
// 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;
}
}