Blob Blame Raw


// TnzCore includes
#include "tsystem.h"
#include "tstream.h"
#include "tthreadmessage.h"
#include "tconvert.h"
#include "tstopwatch.h"
#include "tlevel_io.h"
#include "trasterimage.h"
#include "ttoonzimage.h"
#include "tvectorimage.h"
#include "timagecache.h"
#include "timageinfo.h"
#include "tropcm.h"
#include "tofflinegl.h"
#include "tvectorrenderdata.h"

// TnzBase includes
#include "ttzpimagefx.h"
#include "trasterfx.h"
#include "tzeraryfx.h"
#include "trenderer.h"
#include "tfxcachemanager.h"

// TnzLib includes
#include "toonz/toonzscene.h"
#include "toonz/txsheet.h"
#include "toonz/txshlevelcolumn.h"
#include "toonz/txshpalettecolumn.h"
#include "toonz/txshzeraryfxcolumn.h"
#include "toonz/txshlevel.h"
#include "toonz/txshcell.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/txshpalettelevel.h"
#include "toonz/txshleveltypes.h"
#include "toonz/levelset.h"
#include "toonz/txshchildlevel.h"
#include "toonz/fxdag.h"
#include "toonz/tcolumnfxset.h"
#include "toonz/stage.h"
#include "toonz/fill.h"
#include "toonz/tstageobjectid.h"
#include "toonz/tstageobject.h"
#include "toonz/levelproperties.h"
#include "toonz/imagemanager.h"
#include "toonz/toonzimageutils.h"
#include "toonz/tvectorimageutils.h"
#include "toonz/preferences.h"
#include "toonz/dpiscale.h"
#include "imagebuilders.h"

// 4.6 compatibility - sandor fxs
#include "toonz4.6/raster.h"
#include "sandor_fxs/blend.h"
extern "C" {
#include "sandor_fxs/calligraph.h"
#include "sandor_fxs/patternmap.h"
}

#include "toonz/tcolumnfx.h"

//****************************************************************************************
//    Preliminaries
//****************************************************************************************

#ifdef _WIN32
template class DV_EXPORT_API TFxDeclarationT<TLevelColumnFx>;
template class DV_EXPORT_API TFxDeclarationT<TZeraryColumnFx>;
template class DV_EXPORT_API TFxDeclarationT<TXsheetFx>;
template class DV_EXPORT_API TFxDeclarationT<TOutputFx>;
#endif

TFxDeclarationT<TLevelColumnFx> columnFxInfo(TFxInfo("Toonz_columnFx", true));
TFxDeclarationT<TPaletteColumnFx> paletteColumnFxInfo(
    TFxInfo("Toonz_paletteColumnFx", true));
TFxDeclarationT<TZeraryColumnFx> zeraryColumnFxInfo(
    TFxInfo("Toonz_zeraryColumnFx", true));
TFxDeclarationT<TXsheetFx> infoTXsheetFx(TFxInfo("Toonz_xsheetFx", true));
TFxDeclarationT<TOutputFx> infoTOutputFx(TFxInfo("Toonz_outputFx", true));

//****************************************************************************************
//    Local namespace  -  misc functions
//****************************************************************************************

namespace {

void setMaxMatte(TRasterP r) {
  TRaster32P r32 = (TRaster32P)r;

  TRaster64P r64 = (TRaster64P)r;

  if (r32)
    for (int i = 0; i < r32->getLy(); i++) {
      TPixel *pix = r32->pixels(i);
      for (int j = 0; j < r32->getLx(); j++, pix++) pix->m = 255;
    }
  else if (r64)
    for (int i = 0; i < r64->getLy(); i++) {
      TPixel64 *pix = r64->pixels(i);
      for (int j = 0; j < r64->getLx(); j++, pix++) pix->m = 65535;
    }
}

//---------------------------------------------------------------------------------------------------------

char *strsave(const char *t) {
  char *s;
  s = (char *)malloc(
      strlen(t) +
      1);  // I'm almost sure that this malloc is LEAKED! Please, check that !
  strcpy(s, t);
  return s;
}

//-------------------------------------------------------------------

inline TRect myConvert(const TRectD &r) {
  return TRect(tfloor(r.x0), tfloor(r.y0), tceil(r.x1), tceil(r.y1));
}

//-------------------------------------------------------------------

inline TRect myConvert(const TRectD &r, TPointD &dp) {
  TRect ri(tfloor(r.x0), tfloor(r.y0), tceil(r.x1), tceil(r.y1));
  dp.x = r.x0 - ri.x0;
  dp.y = r.y0 - ri.y0;
  assert(dp.x >= 0 && dp.y >= 0);
  return ri;
}

//--------------------------------------------------

// Currently used on debug only
inline QString traduce(const TRectD &rect) {
  return "[" + QString::number(rect.x0) + " " + QString::number(rect.y0) + " " +
         QString::number(rect.x1) + " " + QString::number(rect.y1) + "]";
}

//--------------------------------------------------

// Currently used on debug only
inline QString traduce(const TRectD &rect, const TRenderSettings &info) {
  return "[" + QString::number(rect.x0) + " " + QString::number(rect.y0) + " " +
         QString::number(rect.x1) + " " + QString::number(rect.y1) +
         "]; aff = (" + QString::number(info.m_affine.a11, 'f') + " " +
         QString::number(info.m_affine.a12, 'f') + " " +
         QString::number(info.m_affine.a13, 'f') + " " +
         QString::number(info.m_affine.a21, 'f') + " " +
         QString::number(info.m_affine.a22, 'f') + " " +
         QString::number(info.m_affine.a23, 'f') + ")";
}

//-------------------------------------------------------------------

inline int colorDistance(const TPixel32 &c0, const TPixel32 &c1) {
  return (c0.r - c1.r) * (c0.r - c1.r) + (c0.g - c1.g) * (c0.g - c1.g) +
         (c0.b - c1.b) * (c0.b - c1.b);
}

//-------------------------------------------------------------------

std::string getAlias(TXsheet *xsh, double frame, const TRenderSettings &info) {
  TFxSet *fxs = xsh->getFxDag()->getTerminalFxs();
  std::string alias;

  // Add the alias for each
  for (int i = 0; i < fxs->getFxCount(); ++i) {
    TRasterFx *fx = dynamic_cast<TRasterFx *>(fxs->getFx(i));
    assert(fx);
    if (!fx) continue;

    alias += fx->getAlias(frame, info) + ";";
  }

  return alias;
}

}  // namespace

//****************************************************************************************
//    Local namespace  -  Colormap (Sandor) Fxs stuff
//****************************************************************************************

static bool vectorMustApplyCmappedFx(
    const std::vector<TRasterFxRenderDataP> &fxs) {
  std::vector<TRasterFxRenderDataP>::const_iterator ft, fEnd(fxs.end());
  for (ft = fxs.begin(); ft != fEnd; ++ft) {
    PaletteFilterFxRenderData *paletteFilterData =
        dynamic_cast<PaletteFilterFxRenderData *>(ft->getPointer());
    SandorFxRenderData *sandorData =
        dynamic_cast<SandorFxRenderData *>(ft->getPointer());

    // (Daniele) Sandor fxs perform on raster colormaps *only* - while texture
    // fxs use palette filters to work.
    // In the latter case, vector-to-colormap conversion makes sure that regions
    // are not rendered under full ink
    // pixels (which causes problems**).
    if (sandorData || (paletteFilterData &&
                       paletteFilterData->m_type != ::eApplyToInksAndPaints))
      return true;

    /*
(Daniele) Disregarding the above reasons - palette filter fxs do not forcibly
return true.

Err... ok, my fault - when I wrote that, I forgot to specify WHICH problems**
occurred. Whops!

Now, it happens that if palette filters DO NOT convert to colormapped forcedly,
special styles can be
retained... so, let's see what happens.

Will have to inquire further, though...
*/
  }

  return false;
}

//-------------------------------------------------------------------

static bool mustApplySandorFx(const std::vector<TRasterFxRenderDataP> &fxs) {
  std::vector<TRasterFxRenderDataP>::const_iterator ft, fEnd(fxs.end());
  for (ft = fxs.begin(); ft != fEnd; ++ft) {
    SandorFxRenderData *sandorData =
        dynamic_cast<SandorFxRenderData *>(ft->getPointer());

    if (sandorData) return true;
  }
  return false;
}

//-------------------------------------------------------------------

static int getEnlargement(const std::vector<TRasterFxRenderDataP> &fxs,
                          double scale) {
  int enlargement = 1;

  std::vector<TRasterFxRenderDataP>::const_iterator ft, fEnd(fxs.end());
  for (ft = fxs.begin(); ft != fEnd; ++ft) {
    SandorFxRenderData *sandorData =
        dynamic_cast<SandorFxRenderData *>(ft->getPointer());

    if (sandorData) {
      switch (sandorData->m_type) {
      case BlendTz: {
        // Nothing happen, unless we have color 0 among the blended ones. In
        // such case,
        // we have to enlarge the bbox proportionally to the amount param.
        std::vector<std::string> items;
        std::string indexes = std::string(sandorData->m_argv[0]);
        parseIndexes(indexes, items);
        PaletteFilterFxRenderData paletteFilterData;
        insertIndexes(items, &paletteFilterData);

        if (!paletteFilterData.m_colors.empty() &&
            *paletteFilterData.m_colors.begin() == 0) {
          BlendTzParams &params = sandorData->m_blendParams;
          enlargement           = params.m_amount * scale;
        }

        break;
      }

      case Calligraphic:
      case OutBorder: {
        CalligraphicParams &params = sandorData->m_callParams;
        enlargement                = params.m_thickness * scale;

        break;
      }

      case ArtAtContour: {
        ArtAtContourParams &params = sandorData->m_contourParams;
        enlargement = std::max(tceil(sandorData->m_controllerBBox.getLx()),
                               tceil(sandorData->m_controllerBBox.getLy())) *
                      params.m_maxSize;

        break;
      }
      }
    }
  }

  return enlargement;
}

//-------------------------------------------------------------------

static void applyPaletteFilter(TPalette *&plt, bool keep,
                               const std::set<int> &colors,
                               const TPalette *srcPlt) {
  if (colors.empty()) return;

  if (!plt) plt = srcPlt->clone();

  if (keep) {
    for (int i = 0; i < plt->getStyleCount(); ++i) {
      if (colors.find(i) == colors.end())
        plt->setStyle(i, TPixel32::Transparent);
    }
  } else {
    std::set<int>::const_iterator ct, cEnd(colors.end());
    for (ct = colors.begin(); ct != cEnd; ++ct) {
      TColorStyle *style = plt->getStyle(*ct);
      if (style) plt->setStyle(*ct, TPixel32::Transparent);
    }
  }
}

//-------------------------------------------------------------------

static TPalette *getPliPalette(const TFilePath &path) {
  TLevelReaderP levelReader = TLevelReaderP(path);
  if (!levelReader.getPointer()) return 0;

  TLevelP level = levelReader->loadInfo();
  TPalette *plt = level->getPalette();

  return plt ? plt->clone() : (TPalette *)0;
}

//-------------------------------------------------------------------

inline bool fxLess(TRasterFxRenderDataP a, TRasterFxRenderDataP b) {
  SandorFxRenderData *sandorDataA =
      dynamic_cast<SandorFxRenderData *>(a.getPointer());
  if (!sandorDataA) return false;

  SandorFxRenderData *sandorDataB =
      dynamic_cast<SandorFxRenderData *>(b.getPointer());
  if (!sandorDataB) return true;

  int aIndex = sandorDataA->m_type == OutBorder
                   ? 2
                   : sandorDataA->m_type == BlendTz ? 1 : 0;
  int bIndex = sandorDataB->m_type == OutBorder
                   ? 2
                   : sandorDataB->m_type == BlendTz ? 1 : 0;

  return aIndex < bIndex;
}

//-------------------------------------------------------------------

inline void sortCmappedFxs(std::vector<TRasterFxRenderDataP> &fxs) {
  std::stable_sort(fxs.begin(), fxs.end(), fxLess);
}

//-------------------------------------------------------------------

static std::vector<int> getAllBut(std::vector<int> &colorIds) {
  assert(TPixelCM32::getMaxInk() == TPixelCM32::getMaxPaint());

  std::vector<int> curColorIds;
  std::sort(colorIds.begin(), colorIds.end());

  // Taking all colors EXCEPT those in colorIds
  unsigned int count1 = 0, count2 = 0;
  int size = TPixelCM32::getMaxInk();

  curColorIds.resize(size + 1 - colorIds.size());
  for (int i = 0; i < size; i++)
    if (count1 < colorIds.size() && colorIds[count1] == i)
      count1++;
    else
      curColorIds[count2++] = i;

  return curColorIds;
}

//-------------------------------------------------------------------

//! \b IMPORTANT \b NOTE: This function is now written so that the passed Toonz
//! Image
//! will be destroyed at the most appropriate time. You should definitely *COPY*
//! all
//! necessary information before calling it - however, since the intent was
//! that of
//! optimizing memory usage, please avoid copying the entire image buffer...

static TImageP applyCmappedFx(TToonzImageP &ti,
                              const std::vector<TRasterFxRenderDataP> &fxs,
                              int frame, double scale) {
  TImageP result = ti;
  TTile resultTile;  // Just a quick wrapper to the ImageCache
  TPalette *inPalette, *tempPlt;
  TPaletteP filteredPalette;
  TRasterCM32P copyRas;
  std::string cmRasCacheId;

  // Retrieve the image dpi
  double dpiX, dpiY;
  ti->getDpi(dpiX, dpiY);
  double dpi = (dpiX > 0) ? dpiX / Stage::inch : 1.0;

  // First, sort the fxs.
  std::vector<TRasterFxRenderDataP> fxsCopy = fxs;
  sortCmappedFxs(fxsCopy);

  // First, let's deal with all fxs working on palettes
  inPalette = ti->getPalette();

  std::vector<TRasterFxRenderDataP>::reverse_iterator it;
  for (it = fxsCopy.rbegin(); it != fxsCopy.rend(); ++it) {
    ExternalPaletteFxRenderData *extpltData =
        dynamic_cast<ExternalPaletteFxRenderData *>((*it).getPointer());
    PaletteFilterFxRenderData *PaletteFilterData =
        dynamic_cast<PaletteFilterFxRenderData *>((*it).getPointer());
    if (extpltData && extpltData->m_palette) {
      filteredPalette = extpltData->m_palette->clone();
      filteredPalette->setFrame(frame);
    } else if (PaletteFilterData &&
               PaletteFilterData->m_type == eApplyToInksAndPaints) {
      bool keep = PaletteFilterData->m_keep;

      std::set<int> colors;
      colors.insert(PaletteFilterData->m_colors.begin(),
                    PaletteFilterData->m_colors.end());

      // Apply the palette filters
      tempPlt = filteredPalette.getPointer();
      applyPaletteFilter(tempPlt, keep, colors, inPalette);
      filteredPalette = tempPlt;

      inPalette = filteredPalette.getPointer();
    }
  }

  if (filteredPalette) {
    // result= ti = ti->clone();   //Is copying truly necessary??
    result = ti = TToonzImageP(ti->getRaster(), ti->getSavebox());
    filteredPalette->setFrame(frame);
    ti->setPalette(filteredPalette.getPointer());
  }

  // Next, deal with fxs working on colormaps themselves
  bool firstSandorFx = true;
  TRasterCM32P cmRas = ti->getRaster();
  TRect tiSaveBox(ti->getSavebox());
  TPaletteP tiPalette(ti->getPalette());
  ti = 0;  // Release the reference to colormap

  // Now, apply cmapped->cmapped fxs
  for (it = fxsCopy.rbegin(); it != fxsCopy.rend(); ++it) {
    PaletteFilterFxRenderData *PaletteFilterData =
        dynamic_cast<PaletteFilterFxRenderData *>(it->getPointer());
    if (PaletteFilterData &&
        PaletteFilterData->m_type != eApplyToInksAndPaints) {
      std::vector<int> indexes;
      indexes.resize(PaletteFilterData->m_colors.size());

      std::set<int>::const_iterator jt = PaletteFilterData->m_colors.begin();
      for (int j = 0; j < (int)indexes.size(); ++j, ++jt) indexes[j] = *jt;

      if (copyRas == TRasterCM32P())
        copyRas =
            cmRas->clone();  // Pixels are literally cleared on a copy buffer

      /*-- 処理するIndexを反転 --*/
      if (PaletteFilterData->m_keep) indexes = getAllBut(indexes);

      /*-- Paintの消去("Lines Including All
       * Areas"のみ、Areaに何も操作をしない) --*/
      if (PaletteFilterData->m_type !=
          eApplyToInksKeepingAllPaints)  // se non e'
                                         // eApplyToInksKeepingAllPaints,
                                         // sicuramente devo cancellare dei
                                         // paint
        TRop::eraseColors(
            copyRas,
            PaletteFilterData->m_type == eApplyToInksDeletingAllPaints
                ? 0
                : &indexes,
            false);

      /*-- Inkの消去 --*/
      if (PaletteFilterData->m_type !=
          eApplyToPaintsKeepingAllInks)  // se non e'
                                         // eApplyToPaintsKeepingAllInks,
                                         // sicuramente devo cancellare degli
                                         // ink
        TRop::eraseColors(
            copyRas,
            PaletteFilterData->m_type == eApplyToPaintsDeletingAllInks
                ? 0
                : &indexes,
            true, PaletteFilterData->m_type == eApplyToInksAndPaints_NoGap);
    }
  }

  if (copyRas) {
    cmRas  = copyRas;
    result = TToonzImageP(cmRas, tiSaveBox);
    result->setPalette(tiPalette.getPointer());
  }

  // Finally, apply cmapped->fullcolor fxs

  // Prefetch all Blend fxs
  std::vector<BlendParam> blendParams;
  for (it = fxsCopy.rbegin(); it != fxsCopy.rend(); ++it) {
    SandorFxRenderData *sandorData =
        dynamic_cast<SandorFxRenderData *>(it->getPointer());
    if (sandorData && sandorData->m_type == BlendTz) {
      BlendParam param;

      param.intensity =
          std::stod(std::string(sandorData->m_argv[3])) * scale * dpi;
      param.smoothness     = sandorData->m_blendParams.m_smoothness;
      param.stopAtCountour = sandorData->m_blendParams.m_noBlending;

      param.superSampling = sandorData->m_blendParams.m_superSampling;

      // Build the color indexes
      std::vector<std::string> items;
      std::string indexes = std::string(sandorData->m_argv[0]);
      parseIndexes(indexes, items);
      PaletteFilterFxRenderData paletteFilterData;
      insertIndexes(items, &paletteFilterData);

      param.colorsIndexes.reserve(paletteFilterData.m_colors.size());

      std::set<int>::iterator it;
      for (it = paletteFilterData.m_colors.begin();
           it != paletteFilterData.m_colors.end(); ++it)
        param.colorsIndexes.push_back(*it);

      blendParams.push_back(param);
    }
  }

  // Apply each sandor
  for (it = fxsCopy.rbegin(); it != fxsCopy.rend(); ++it) {
    SandorFxRenderData *sandorData =
        dynamic_cast<SandorFxRenderData *>(it->getPointer());

    if (sandorData) {
      // Avoid dealing with specific cases
      if ((sandorData->m_type == BlendTz && blendParams.empty()) ||
          (sandorData->m_type == OutBorder && !firstSandorFx))
        continue;

      if (!firstSandorFx) {
        // Retrieve the colormap from cache
        cmRas = TToonzImageP(TImageCache::instance()->get(cmRasCacheId, true))
                    ->getRaster();

        // Apply a palette filter in order to keep only the colors specified in
        // the sandor argv
        std::vector<std::string> items;
        std::string indexes = std::string(sandorData->m_argv[0]);
        parseIndexes(indexes, items);
        PaletteFilterFxRenderData paletteFilterData;
        insertIndexes(items, &paletteFilterData);

        filteredPalette = tempPlt = 0;
        applyPaletteFilter(tempPlt, true, paletteFilterData.m_colors,
                           tiPalette.getPointer());
        filteredPalette = tempPlt;
      } else {
        // Pass the input colormap to the cache and release its reference as
        // final result
        cmRasCacheId = TImageCache::instance()->getUniqueId();
        TImageCache::instance()->add(cmRasCacheId,
                                     TToonzImageP(cmRas, tiSaveBox));
        result = 0;
      }

      // Convert current smart pointers to a 4.6 'fashion'. The former ones are
      // released - so they
      // do not occupy smart object references.
      RASTER *oldRasterIn, *oldRasterOut;
      oldRasterIn = TRop::convertRaster50to46(
          cmRas, filteredPalette ? filteredPalette : tiPalette.getPointer());
      cmRas = TRasterCM32P(0);
      {
        TRaster32P rasterOut(TDimension(oldRasterIn->lx, oldRasterIn->ly));
        oldRasterOut = TRop::convertRaster50to46(rasterOut, 0);
      }

      switch (sandorData->m_type) {
      case BlendTz: {
        if (blendParams.empty()) continue;

        // Retrieve the colormap from cache
        cmRas = TToonzImageP(TImageCache::instance()->get(cmRasCacheId, true))
                    ->getRaster();

        TToonzImageP ti(cmRas, tiSaveBox);
        ti->setPalette(filteredPalette ? filteredPalette.getPointer()
                                       : tiPalette.getPointer());

        TRasterImageP riOut(TImageCache::instance()->get(
            std::string(oldRasterOut->cacheId, oldRasterOut->cacheIdLength),
            true));
        TRaster32P rasterOut = riOut->getRaster();

        blend(ti, rasterOut, blendParams);

        blendParams.clear();
        break;
      }

      case Calligraphic:
      case OutBorder: {
        if (sandorData->m_type == OutBorder && !firstSandorFx) continue;

        const char *argv[12];
        memcpy(argv, sandorData->m_argv, 12 * sizeof(const char *));

        double thickness =
            std::stod(std::string(sandorData->m_argv[7])) * scale * dpi;
        argv[7] = strsave(std::to_string(thickness).c_str());

        calligraph(oldRasterIn, oldRasterOut, sandorData->m_border,
                   sandorData->m_argc, argv, sandorData->m_shrink,
                   sandorData->m_type == OutBorder);

        break;
      }
      case ArtAtContour: {
        const char *argv[12];
        memcpy(argv, sandorData->m_argv, 12 * sizeof(const char *));

        double distance =
            std::stod(std::string(sandorData->m_argv[6])) * scale * dpi;
        argv[6]  = strsave(std::to_string(distance).c_str());
        distance = std::stod(std::string(sandorData->m_argv[7])) * scale * dpi;
        argv[7]  = strsave(std::to_string(distance).c_str());
        double density =
            std::stod(std::string(sandorData->m_argv[8])) / sq(scale * dpi);
        argv[8] = strsave(std::to_string(density).c_str());
        double size =
            std::stod(std::string(sandorData->m_argv[1])) * scale * dpi;
        argv[1] = strsave(std::to_string(size).c_str());
        size    = std::stod(std::string(sandorData->m_argv[2])) * scale * dpi;
        argv[2] = strsave(std::to_string(size).c_str());
        RASTER *imgContour =
            TRop::convertRaster50to46(sandorData->m_controller, 0);
        patternmap(oldRasterIn, oldRasterOut, sandorData->m_border,
                   sandorData->m_argc, argv, sandorData->m_shrink, imgContour);
        TRop::releaseRaster46(imgContour);

        break;
      }
      default:
        assert(false);
      }

      TRasterImageP riOut(TImageCache::instance()->get(
          std::string(oldRasterOut->cacheId, oldRasterOut->cacheIdLength),
          true));
      TRaster32P rasterOut = riOut->getRaster();

      TRop::releaseRaster46(oldRasterIn);
      TRop::releaseRaster46(oldRasterOut);

      if (firstSandorFx) {
        resultTile.setRaster(rasterOut);
        firstSandorFx = false;
      } else
        TRop::over(resultTile.getRaster(), rasterOut);
    }
  }

  // Release cmRas cache identifier if any
  TImageCache::instance()->remove(cmRasCacheId);

  if (!result) result = TRasterImageP(resultTile.getRaster());

  return result;
}

//-------------------------------------------------------------------

static void applyCmappedFx(TVectorImageP &vi,
                           const std::vector<TRasterFxRenderDataP> &fxs,
                           int frame) {
  TRasterP ras;
  bool keep = false;
  TPaletteP modPalette;
  std::set<int> colors;
  std::vector<TRasterFxRenderDataP>::const_iterator it = fxs.begin();

  // prima tutti gli effetti che agiscono sulla paletta....
  for (; it != fxs.end(); ++it) {
    ExternalPaletteFxRenderData *extpltData =
        dynamic_cast<ExternalPaletteFxRenderData *>((*it).getPointer());
    PaletteFilterFxRenderData *pltFilterData =
        dynamic_cast<PaletteFilterFxRenderData *>((*it).getPointer());

    if (extpltData && extpltData->m_palette)
      modPalette = extpltData->m_palette->clone();
    else if (pltFilterData) {
      assert(
          pltFilterData->m_type ==
          eApplyToInksAndPaints);  // Must have been converted to CM32 otherwise

      keep = pltFilterData->m_keep;
      colors.insert(pltFilterData->m_colors.begin(),
                    pltFilterData->m_colors.end());
    }
  }

  TPalette *tempPlt = modPalette.getPointer();
  applyPaletteFilter(tempPlt, keep, colors, vi->getPalette());
  modPalette = tempPlt;

  if (modPalette) {
    vi = vi->clone();
    vi->setPalette(modPalette.getPointer());
  }
}

//****************************************************************************************
//    LevelFxResourceBuilder  definition
//****************************************************************************************

class LevelFxBuilder final : public ResourceBuilder {
  TRasterP m_loadedRas;
  TPaletteP m_palette;

  TXshSimpleLevel *m_sl;
  TFrameId m_fid;
  TRectD m_tileGeom;
  bool m_64bit;

  TRect m_rasBounds;

public:
  LevelFxBuilder(const std::string &resourceName, double frame,
                 const TRenderSettings &rs, TXshSimpleLevel *sl, TFrameId fid)
      : ResourceBuilder(resourceName, 0, frame, rs)
      , m_loadedRas()
      , m_palette()
      , m_sl(sl)
      , m_fid(fid)
      , m_64bit(rs.m_bpp == 64) {}

  void setRasBounds(const TRect &rasBounds) { m_rasBounds = rasBounds; }

  void compute(const TRectD &tileRect) override {
    // Load the image
    TImageP img(m_sl->getFullsampledFrame(
        m_fid, (m_64bit ? ImageManager::is64bitEnabled : 0) |
                   ImageManager::dontPutInCache));

    if (!img) return;

    TRasterImageP rimg(img);
    TToonzImageP timg(img);

    m_loadedRas = rimg ? (TRasterP)rimg->getRaster()
                       : timg ? (TRasterP)timg->getRaster() : TRasterP();
    assert(m_loadedRas);

    if (timg) m_palette = timg->getPalette();

    assert(tileRect ==
           TRectD(0, 0, m_loadedRas->getLx(), m_loadedRas->getLy()));
  }

  void simCompute(const TRectD &rect) override {}

  void upload(TCacheResourceP &resource) override {
    assert(m_loadedRas);
    resource->upload(TPoint(), m_loadedRas);
    if (m_palette) resource->uploadPalette(m_palette);
  }

  bool download(TCacheResourceP &resource) override {
    // If the image has been loaded in this builder, just use it
    if (m_loadedRas) return true;

    // If the image has yet to be loaded by this builder, skip without
    // allocating anything
    if (resource->canDownloadAll(m_rasBounds)) {
      m_loadedRas = resource->buildCompatibleRaster(m_rasBounds.getSize());
      resource->downloadPalette(m_palette);
      return resource->downloadAll(TPoint(), m_loadedRas);
    } else
      return false;
  }

  TImageP getImage() const {
    if (!m_loadedRas) return TImageP();

    TRasterCM32P cm(m_loadedRas);

    TImageP result(cm ? TImageP(TToonzImageP(cm, cm->getBounds()))
                      : TImageP(TRasterImageP(m_loadedRas)));
    if (m_palette) result->setPalette(m_palette.getPointer());

    return result;
  }
};

//****************************************************************************************
//    TLevelColumnFx  implementation
//****************************************************************************************

TLevelColumnFx::TLevelColumnFx()
    : m_levelColumn(0), m_isCachable(true), m_mutex(), m_offlineContext(0) {
  setName(L"LevelColumn");
}

//--------------------------------------------------

TLevelColumnFx::~TLevelColumnFx() {
  if (m_offlineContext) delete m_offlineContext;
}

//-------------------------------------------------------------------

bool TLevelColumnFx::canHandle(const TRenderSettings &info, double frame) {
  // NOTE 1: Currently, it is necessary that level columns return FALSE for
  // raster levels - just a quick way to tell the cache functions that they
  // have to be cached.

  if (!m_levelColumn) return true;

  TXshCell cell = m_levelColumn->getCell(m_levelColumn->getFirstRow());
  if (cell.isEmpty()) return true;

  TXshSimpleLevel *sl = cell.m_level->getSimpleLevel();
  if (!sl) return true;

  return (sl->getType() == PLI_XSHLEVEL &&
          !vectorMustApplyCmappedFx(info.m_data));
}

//-------------------------------------------------------------------

TAffine TLevelColumnFx::handledAffine(const TRenderSettings &info,
                                      double frame) {
  if (!m_levelColumn) return TAffine();

  TXshCell cell = m_levelColumn->getCell(m_levelColumn->getFirstRow());
  if (cell.isEmpty()) return TAffine();

  TXshSimpleLevel *sl = cell.m_level->getSimpleLevel();
  if (!sl) return TAffine();

  if (sl->getType() == PLI_XSHLEVEL)
    return vectorMustApplyCmappedFx(info.m_data)
               ? TRasterFx::handledAffine(info, frame)
               : info.m_affine;

  // Accept any translation consistent with the image's pixels geometry
  TImageInfo imageInfo;
  getImageInfo(imageInfo, sl, cell.m_frameId);

  TPointD pixelsOrigin(-0.5 * imageInfo.m_lx, -0.5 * imageInfo.m_ly);

  const TAffine &aff = info.m_affine;
  if (aff.a11 != 1.0 || aff.a22 != 1.0 || aff.a12 != 0.0 || aff.a21 != 0.0)
    return TTranslation(-pixelsOrigin);

  // This is a translation, ok. Just ensure it is consistent.
  TAffine consistentAff(aff);

  consistentAff.a13 -= pixelsOrigin.x, consistentAff.a23 -= pixelsOrigin.y;
  consistentAff.a13 = tfloor(consistentAff.a13),
  consistentAff.a23 = tfloor(consistentAff.a23);
  consistentAff.a13 += pixelsOrigin.x, consistentAff.a23 += pixelsOrigin.y;

  return consistentAff;
}

//-------------------------------------------------------------------

TFilePath TLevelColumnFx::getPalettePath(int frame) const {
  if (!m_levelColumn) return TFilePath();

  TXshCell cell = m_levelColumn->getCell(frame);
  if (cell.isEmpty()) return TFilePath();

  TXshSimpleLevel *sl = cell.m_level->getSimpleLevel();
  if (!sl) return TFilePath();

  if (sl->getType() == TZP_XSHLEVEL)
    return sl->getScene()->decodeFilePath(
        sl->getPath().withNoFrame().withType("tpl"));

  if (sl->getType() == PLI_XSHLEVEL)
    return sl->getScene()->decodeFilePath(sl->getPath());

  return TFilePath();
}

//-------------------------------------------------------------------

TPalette *TLevelColumnFx::getPalette(int frame) const {
  if (!m_levelColumn) return 0;

  TXshCell cell = m_levelColumn->getCell(frame);
  if (cell.isEmpty()) return 0;

  TXshSimpleLevel *sl = cell.m_level->getSimpleLevel();
  if (!sl) return 0;

  return sl->getPalette();
}

//--------------------------------------------------

TFx *TLevelColumnFx::clone(bool recursive) const {
  TLevelColumnFx *clonedFx =
      dynamic_cast<TLevelColumnFx *>(TFx::clone(recursive));
  assert(clonedFx);
  clonedFx->m_levelColumn = m_levelColumn;
  clonedFx->m_isCachable  = m_isCachable;
  return clonedFx;
}

//--------------------------------------------------

void TLevelColumnFx::doDryCompute(TRectD &rect, double frame,
                                  const TRenderSettings &info) {
  if (!m_levelColumn) return;

  int row       = (int)frame;
  TXshCell cell = m_levelColumn->getCell(row);
  if (cell.isEmpty()) return;

  TXshSimpleLevel *sl = cell.m_level->getSimpleLevel();
  if (!sl) return;

  // In case this is a vector level, the image is renderized quickly and
  // directly at the
  // correct resolution. Caching is disabled in such case, at the moment.
  if (sl->getType() == PLI_XSHLEVEL) return;

  int renderStatus =
      TRenderer::instance().getRenderStatus(TRenderer::renderId());

  std::string alias = getAlias(frame, TRenderSettings()) + "_image";

  TImageInfo imageInfo;
  getImageInfo(imageInfo, sl, cell.m_frameId);
  TRectD imgRect(0, 0, imageInfo.m_lx, imageInfo.m_ly);

  if (renderStatus == TRenderer::FIRSTRUN) {
    ResourceBuilder::declareResource(alias, 0, imgRect, frame, info, false);
  } else {
    LevelFxBuilder builder(alias, frame, info, sl, cell.m_frameId);
    builder.setRasBounds(TRect(0, 0, imageInfo.m_lx - 1, imageInfo.m_ly - 1));
    builder.simBuild(imgRect);
  }
}

//--------------------------------------------------

bool isSubsheetChainOnColumn0(TXsheet *topXsheet, TXsheet *subsheet,
                              int frame) {
  if (topXsheet == subsheet) return true;

  const TXshCell cell = topXsheet->getCell(frame, 0);
  if (!cell.m_level) return false;
  TXshChildLevel *cl = cell.m_level->getChildLevel();
  if (!cl) return false;
  return isSubsheetChainOnColumn0(cl->getXsheet(), subsheet, frame);
}

//-----------------------------------------------------

void TLevelColumnFx::doCompute(TTile &tile, double frame,
                               const TRenderSettings &info) {
  if (!m_levelColumn) return;

  // Ensure that a corresponding cell and level exists
  int row       = (int)frame;
  TXshCell cell = m_levelColumn->getCell(row);

  if (cell.isEmpty()) return;

  TXshSimpleLevel *sl = cell.m_level->getSimpleLevel();
  if (!sl) return;

  TFrameId fid = cell.m_frameId;

  TImageP img;
  TImageInfo imageInfo;

  // Now, fetch the image
  if (sl->getType() != PLI_XSHLEVEL) {
    // Raster case
    LevelFxBuilder builder(getAlias(frame, TRenderSettings()) + "_image", frame,
                           info, sl, fid);

    getImageInfo(imageInfo, sl, fid);
    TRectD imgRect(0, 0, imageInfo.m_lx, imageInfo.m_ly);

    builder.setRasBounds(TRect(0, 0, imageInfo.m_lx - 1, imageInfo.m_ly - 1));
    builder.build(imgRect);

    img = builder.getImage();
  } else {
    // Vector case (loading is immediate)
    if (!img) {
      img = sl->getFullsampledFrame(
          fid, ((info.m_bpp == 64) ? ImageManager::is64bitEnabled : 0) |
                   ImageManager::dontPutInCache);
    }
  }

  // Extract the required geometry
  TRect tileBounds(tile.getRaster()->getBounds());
  TRectD tileRectD = TRectD(tileBounds.x0, tileBounds.y0, tileBounds.x1 + 1,
                            tileBounds.y1 + 1) +
                     tile.m_pos;

  // To be sure, if there is no image, return.
  if (!img) return;

  TRectD bBox = img->getBBox();

  // Discriminate image type
  if (TVectorImageP vectorImage = img) {
    // Vector case

    if (vectorMustApplyCmappedFx(info.m_data)) {
      // Deal separately
      applyTzpFxsOnVector(vectorImage, tile, frame, info);
    } else {
      QMutexLocker m(&m_mutex);
      bBox = info.m_affine * vectorImage->getBBox();
      TDimension size(tile.getRaster()->getSize());

      TAffine aff = TTranslation(-tile.m_pos.x, -tile.m_pos.y) *
                    TScale(1.0 / info.m_shrinkX, 1.0 / info.m_shrinkY) *
                    info.m_affine;

      applyCmappedFx(vectorImage, info.m_data, (int)frame);
      TPalette *vpalette = vectorImage->getPalette();
      assert(vpalette);
      m_isCachable = !vpalette->isAnimated();
      int oldFrame = vpalette->getFrame();

      TVectorRenderData rd(TVectorRenderData::ProductionSettings(), aff,
                           TRect(size), vpalette);

      if (!m_offlineContext || m_offlineContext->getLx() < size.lx ||
          m_offlineContext->getLy() < size.ly) {
        if (m_offlineContext) delete m_offlineContext;
        m_offlineContext = new TOfflineGL(size);
      }

      m_offlineContext->makeCurrent();
      m_offlineContext->clear(TPixel32(0, 0, 0, 0));

      // If level has animated palette, it is necessary to lock palette's color
      // against
      // concurrents TPalette::setFrame.
      if (!m_isCachable) vpalette->mutex()->lock();

      vpalette->setFrame((int)frame);
      m_offlineContext->draw(vectorImage, rd, true);
      vpalette->setFrame(oldFrame);

      if (!m_isCachable) vpalette->mutex()->unlock();

      m_offlineContext->getRaster(tile.getRaster());

      m_offlineContext->doneCurrent();
    }
  } else {
    // Raster case

    TRasterP ras;
    TAffine aff;

    TRasterImageP ri = img;
    TToonzImageP ti  = img;
    img              = 0;

    if (ri) {
      // Fullcolor case

      ras = ri->getRaster();
      ri  = 0;
      TRaster32P ras32(ras);
      TRaster64P ras64(ras);

      // Ensure that ras is either a 32 or 64 fullcolor.
      // Otherwise, we have to convert it.
      if (!ras32 && !ras64) {
        TRasterP tileRas(tile.getRaster());
        TRaster32P tileRas32(tileRas);
        TRaster64P tileRas64(tileRas);

        if (tileRas32) {
          ras32 = TRaster32P(ras->getLx(), ras->getLy());
          TRop::convert(ras32, ras);
          ras = ras32;
        } else if (tileRas64) {
          ras64 = TRaster64P(ras->getLx(), ras->getLy());
          TRop::convert(ras64, ras);
          ras = ras64;
        } else
          assert(0);
      }
      TXsheet *xsh  = m_levelColumn->getLevelColumn()->getXsheet();
      TXsheet *txsh = sl->getScene()->getTopXsheet();

      LevelProperties *levelProp = sl->getProperties();
      if (Preferences::instance()->isIgnoreAlphaonColumn1Enabled() &&
          m_levelColumn->getIndex() == 0 &&
          isSubsheetChainOnColumn0(txsh, xsh, frame)) {
        TRasterP appRas(ras->clone());
        setMaxMatte(appRas);
        ras = appRas;
      }
      if (levelProp->doPremultiply()) {
        TRasterP appRas = ras->clone();
        TRop::premultiply(appRas);
        ras = appRas;
      }
      if (levelProp->whiteTransp()) {
        TRasterP appRas(ras->clone());
        TRop::whiteTransp(appRas);
        ras = appRas;
      }
      if (levelProp->antialiasSoftness() > 0) {
        TRasterP appRas = ras->create(ras->getLx(), ras->getLy());
        TRop::antialias(ras, appRas, 10, levelProp->antialiasSoftness());
        ras = appRas;
      }

      if (TXshSimpleLevel::m_fillFullColorRaster) {
        TRasterP appRas(ras->clone());
        FullColorAreaFiller filler(appRas);
        TPaletteP palette = new TPalette();
        int styleId       = palette->getPage(0)->addStyle(TPixel32::White);
        FillParameters params;
        params.m_palette      = palette.getPointer();
        params.m_styleId      = styleId;
        params.m_minFillDepth = 0;
        params.m_maxFillDepth = 15;
        filler.rectFill(appRas->getBounds(), params, false);
        ras = appRas;
      }

    } else if (ti) {
      // Colormap case

      // Use the imageInfo's dpi
      ti->setDpi(imageInfo.m_dpix, imageInfo.m_dpiy);

      TImageP newImg = applyTzpFxs(ti, frame, info);

      ri = newImg;
      ti = newImg;

      if (ri)
        ras = ri->getRaster();
      else
        ras = ti->getRaster();

      if (sl->getProperties()->antialiasSoftness() > 0) {
        // convert colormap raster to fullcolor raster before applying antialias
        if (ti) {
          TRaster32P convRas(ras->getSize());
          TRop::convert(convRas, ras, ti->getPalette(), TRect(), false, true);
          ras = convRas;
        }
        TRasterP appRas = ras->create(ras->getLx(), ras->getLy());
        TRop::antialias(ras, appRas, 10,
                        sl->getProperties()->antialiasSoftness());
        ras = appRas;
      }

      ri = 0;
    }

    if (ras) {
      double lx_2 = ras->getLx() / 2.0;
      double ly_2 = ras->getLy() / 2.0;

      TRenderSettings infoAux(info);
      assert(info.m_affine.isTranslation());
      infoAux.m_data.clear();

      // Place the output rect in the image's reference
      tileRectD += TPointD(lx_2 - info.m_affine.a13, ly_2 - info.m_affine.a23);

      // Then, retrieve loaded image's interesting region
      TRectD inTileRectD;
      if (ti) {
        TRect saveBox(ti->getSavebox());
        inTileRectD =
            TRectD(saveBox.x0, saveBox.y0, saveBox.x1 + 1, saveBox.y1 + 1);
      } else {
        TRect rasBounds(ras->getBounds());
        inTileRectD = TRectD(rasBounds.x0, rasBounds.y0, rasBounds.x1 + 1,
                             rasBounds.y1 + 1);
      }

      // And intersect the two
      inTileRectD *= tileRectD;

      // Should the intersection be void, return
      if (inTileRectD.x0 >= inTileRectD.x1 || inTileRectD.y0 >= inTileRectD.y1)
        return;

      // Output that intersection in the requested tile
      TRect inTileRect(tround(inTileRectD.x0), tround(inTileRectD.y0),
                       tround(inTileRectD.x1) - 1, tround(inTileRectD.y1) - 1);
      TTile inTile(ras->extract(inTileRect),
                   inTileRectD.getP00() + TPointD(-lx_2, -ly_2));

      // Observe that inTile is in the standard reference, ie image's minus the
      // center coordinates

      if ((TRasterCM32P)ras) {
        // In the colormapped case, we have to convert the cmap to fullcolor
        TPalette *palette = ti->getPalette();

        if (!m_isCachable) palette->mutex()->lock();

        int prevFrame = palette->getFrame();

        palette->setFrame((int)frame);

        TTile auxtile(TRaster32P(inTile.getRaster()->getSize()), inTile.m_pos);
        TRop::convert(auxtile, inTile, palette, false, true);
        palette->setFrame(prevFrame);

        if (!m_isCachable) palette->mutex()->unlock();

        inTile.setRaster(auxtile.getRaster());
      }

      TRasterFx::applyAffine(tile, inTile, infoAux);
    }
  }
}

//-------------------------------------------------------------------

TImageP TLevelColumnFx::applyTzpFxs(TToonzImageP &ti, double frame,
                                    const TRenderSettings &info) {
  double scale = sqrt(fabs(info.m_affine.det()));

  int prevFrame = ti->getPalette()->getFrame();
  m_isCachable  = !ti->getPalette()->isAnimated();

  if (!m_isCachable) ti->getPalette()->mutex()->lock();

  TPaletteP palette(ti->getPalette());
  palette->setFrame((int)frame);
  TImageP newImg = applyCmappedFx(ti, info.m_data, (int)frame, scale);
  palette->setFrame(prevFrame);

  if (!m_isCachable) palette->mutex()->unlock();

  return newImg;
}

//-------------------------------------------------------------------

void TLevelColumnFx::applyTzpFxsOnVector(const TVectorImageP &vi, TTile &tile,
                                         double frame,
                                         const TRenderSettings &info) {
  TRect tileBounds(tile.getRaster()->getBounds());
  TRectD tileRectD = TRectD(tileBounds.x0, tileBounds.y0, tileBounds.x1 + 1,
                            tileBounds.y1 + 1) +
                     tile.m_pos;

  // Info's affine is not the identity only in case the loaded image is a vector
  // one.
  double scale    = sqrt(fabs(info.m_affine.det()));
  int enlargement = getEnlargement(info.m_data, scale);

  // Extract the vector image's groups
  std::vector<TVectorImageP> groupsList;
  getGroupsList(vi, groupsList);

  // For each group, apply the tzp fxs stored in info. The result is immediately
  // converted
  // to a raster image if necessary.
  unsigned int i;
  for (i = 0; i < groupsList.size(); ++i) {
    TVectorImageP &groupVi = groupsList[i];

    // Extract the group's bbox.
    TRectD groupBBox(info.m_affine * groupVi->getBBox());
    if (!mustApplySandorFx(info.m_data)) groupBBox *= tileRectD;

    groupBBox = groupBBox.enlarge(enlargement);

    if (groupBBox.x0 >= groupBBox.x1 || groupBBox.y0 >= groupBBox.y1) continue;

    // Ensure that groupBBox and tile have the same integer geometry
    groupBBox -= tile.m_pos;
    groupBBox.x0 = tfloor(groupBBox.x0);
    groupBBox.y0 = tfloor(groupBBox.y0);
    groupBBox.x1 = tceil(groupBBox.x1);
    groupBBox.y1 = tceil(groupBBox.y1);
    groupBBox += tile.m_pos;

    // Build groupBBox's relative position to the tile
    TPoint groupRelativePosToTile(groupBBox.x0 - tile.m_pos.x,
                                  groupBBox.y0 - tile.m_pos.y);

    // Convert the group to a strictly sufficient Toonz image
    TToonzImageP groupTi = ToonzImageUtils::vectorToToonzImage(
        groupVi, info.m_affine, groupVi->getPalette(), groupBBox.getP00(),
        TDimension(groupBBox.getLx(), groupBBox.getLy()), &info.m_data, true);

    // Apply the tzp fxs to the converted Toonz image
    TImageP groupResult = applyTzpFxs(groupTi, frame, info);

    // If necessary, convert the result to fullcolor
    TRasterImageP groupRi = groupResult;
    if (!groupRi) {
      groupTi = groupResult;
      assert(groupTi);

      TRasterP groupTiRas(groupTi->getRaster());
      TRaster32P tempRas(groupTiRas->getSize());
      groupRi = TRasterImageP(tempRas);

      TRop::convert(tempRas, groupTiRas, groupTi->getPalette());
    }

    // Over the group image on the output
    TRasterP tileRas(tile.getRaster());
    TRop::over(tileRas, groupRi->getRaster(), groupRelativePosToTile);
  }
}

//-------------------------------------------------------------------

int TLevelColumnFx::getMemoryRequirement(const TRectD &rect, double frame,
                                         const TRenderSettings &info) {
  // Sandor fxs are currently considered *VERY* inefficient upon tile
  // subdivision
  if (mustApplySandorFx(info.m_data)) {
    return -1;
  }
  return 0;
}

//-------------------------------------------------------------------

void TLevelColumnFx::getImageInfo(TImageInfo &info, TXshSimpleLevel *sl,
                                  TFrameId frameId) {
  int type = sl->getType();
  assert(type != PLI_XSHLEVEL);
  if (type == PLI_XSHLEVEL) return;

  std::string imageId = sl->getImageId(frameId);

  const TImageInfo *storedInfo =
      ImageManager::instance()->getInfo(imageId, ImageManager::none, 0);

  if (!storedInfo)  // sulle pict caricate info era nullo, ma l'immagine c'e'!
  // con la getFullSampleFrame riprendo   l'immagine e ricalcolo la savebox...
  {
    TImageP img;
    if (!(img =
              sl->getFullsampledFrame(frameId, ImageManager::dontPutInCache))) {
      assert(false);
      return;
    }
    // Raster levels from ffmpeg were not giving the right dimensions without
    // the raster cast and check
    TRasterImageP rasterImage = (TRasterImageP)img;
    if (rasterImage) {
      info.m_lx = (int)rasterImage->getRaster()->getLx();
      info.m_ly = (int)rasterImage->getRaster()->getLy();
    } else {
      info.m_lx = (int)img->getBBox().getLx();
      info.m_ly = (int)img->getBBox().getLy();
    }
    info.m_x0 = info.m_y0 = 0;
    info.m_x1             = (int)img->getBBox().getP11().x;
    info.m_y1             = (int)img->getBBox().getP11().y;
  } else
    info = *storedInfo;
}

//-------------------------------------------------------------------

bool TLevelColumnFx::doGetBBox(double frame, TRectD &bBox,
                               const TRenderSettings &info) {
  // Usual preliminaries (make sure a level/cell exists, etc...)
  if (!m_levelColumn) return false;

  int row              = (int)frame;
  const TXshCell &cell = m_levelColumn->getCell(row);
  if (cell.isEmpty()) return false;

  TXshLevelP xshl = cell.m_level;
  if (!xshl) return false;

  TXshSimpleLevel *sl = xshl->getSimpleLevel();
  if (!sl) return false;

  double dpi = 1.0;

  // Discriminate for level type
  int type = xshl->getType();
  if (type != PLI_XSHLEVEL) {
    TImageInfo imageInfo;
    getImageInfo(imageInfo, sl, cell.m_frameId);

    double cx = 0.5 * imageInfo.m_lx;
    double cy = 0.5 * imageInfo.m_ly;
    if (info.m_getFullSizeBBox) {
      bBox = TRectD(-cx, -cy, cx, cy);
    } else {
      TRect imageSavebox(imageInfo.m_x0, imageInfo.m_y0, imageInfo.m_x1,
                         imageInfo.m_y1);
      double x0 = (imageSavebox.x0 - cx);
      double y0 = (imageSavebox.y0 - cy);
      double x1 = x0 + imageSavebox.getLx();
      double y1 = y0 + imageSavebox.getLy();
      bBox      = TRectD(x0, y0, x1, y1);
    }
    dpi = imageInfo.m_dpix / Stage::inch;
  } else {
    TImageP img = m_levelColumn->getCell(row).getImage(false);
    if (!img) return false;
    bBox = img->getBBox();
  }

  // Add the enlargement of the bbox due to Tzp render data
  if (info.m_data.size()) {
    TRectD imageBBox(bBox);
    for (unsigned int i = 0; i < info.m_data.size(); ++i) {
      TRectD enlargedImageBBox = info.m_data[i]->getBBoxEnlargement(imageBBox);
      double enlargement       = enlargedImageBBox.x1 - imageBBox.x1;
      bBox += imageBBox.enlarge(enlargement * dpi);
    }
  }

  return true;
}

//-------------------------------------------------------------------

const TPersistDeclaration *TLevelColumnFx::getDeclaration() const {
  return &columnFxInfo;
}

//-------------------------------------------------------------------

std::string TLevelColumnFx::getPluginId() const { return "Toonz_"; }

//-------------------------------------------------------------------

TFxTimeRegion TLevelColumnFx::getTimeRegion() const {
  if (!m_levelColumn) return TFxTimeRegion();

  int first = m_levelColumn->getFirstRow();
  int last  = m_levelColumn->getRowCount();

  return TFxTimeRegion(first, last);
}

//-------------------------------------------------------------------

void TLevelColumnFx::setColumn(TXshLevelColumn *column) {
  m_levelColumn = column;
}

//-------------------------------------------------------------------

std::wstring TLevelColumnFx::getColumnId() const {
  if (!m_levelColumn) return L"Col?";
  return L"Col" + std::to_wstring(m_levelColumn->getIndex() + 1);
}

//-------------------------------------------------------------------

std::wstring TLevelColumnFx::getColumnName() const {
  if (!m_levelColumn) return L"";
  int idx = getColumnIndex();
  return ::to_wstring(m_levelColumn->getXsheet()
                          ->getStageObject(TStageObjectId::ColumnId(idx))
                          ->getName());
}

//-------------------------------------------------------------------

std::string TLevelColumnFx::getAlias(double frame,
                                     const TRenderSettings &info) const {
  if (!m_levelColumn || m_levelColumn->getCell((int)frame).isEmpty())
    return std::string();

  const TXshCell &cell = m_levelColumn->getCell((int)frame);

  TFilePath fp;
  TXshSimpleLevel *sl = cell.getSimpleLevel();

  if (!sl) {
    // Try with the sub-xsheet case
    TXshChildLevel *childLevel = cell.m_level->getChildLevel();
    if (childLevel) return ::getAlias(childLevel->getXsheet(), frame, info);

    return std::string();
  }

  TFilePath path = sl->getPath();
  if (!cell.m_frameId.isNoFrame())
    fp = path.withFrame(cell.m_frameId);
  else
    fp = path;

  std::string rdata;
  std::vector<TRasterFxRenderDataP>::const_iterator it = info.m_data.begin();
  for (; it != info.m_data.end(); ++it) {
    TRasterFxRenderDataP data = *it;
    if (data) rdata += data->toString();
  }

  if (sl->getType() == PLI_XSHLEVEL || sl->getType() == TZP_XSHLEVEL) {
    TPalette *palette = cell.getPalette();
    if (palette && palette->isAnimated())
      rdata += "animatedPlt" + std::to_string(frame);
  }

  if (Preferences::instance()->isIgnoreAlphaonColumn1Enabled()) {
    TXsheet *xsh  = m_levelColumn->getLevelColumn()->getXsheet();
    TXsheet *txsh = sl->getScene()->getTopXsheet();

    if (m_levelColumn->getIndex() == 0 &&
        isSubsheetChainOnColumn0(txsh, xsh, frame))
      rdata += "column_0";
  }

  return getFxType() + "[" + ::to_string(fp.getWideString()) + "," + rdata +
         "]";
}

//-------------------------------------------------------------------

int TLevelColumnFx::getColumnIndex() const {
  return m_levelColumn ? m_levelColumn->getIndex() : -1;
}

//-------------------------------------------------------------------

TXshColumn *TLevelColumnFx::getXshColumn() const { return m_levelColumn; }

//-------------------------------------------------------------------

TAffine TLevelColumnFx::getDpiAff(int frame) {
  if (!m_levelColumn) return TAffine();

  TXshCell cell = m_levelColumn->getCell(frame);
  if (cell.isEmpty()) return TAffine();

  TXshSimpleLevel *sl = cell.m_level->getSimpleLevel();
  TFrameId fid        = cell.m_frameId;

  if (sl) return ::getDpiAffine(sl, cell.m_frameId, true);

  return TAffine();
}

//-------------------------------------------------------------------

void TLevelColumnFx::saveData(TOStream &os) {
  assert(m_levelColumn);
  TFx::saveData(os);
}

//-------------------------------------------------------------------

void TLevelColumnFx::loadData(TIStream &is) {
  // The internal numeric identifier is set here but not in constructors.
  // This is *intended* as fx cloning needs to retain the original id
  // through the fxs tree deployment procedure happening just before a
  // render process (see scenefx.cpp).

  TFx::loadData(is);
  setNewIdentifier();
}

//****************************************************************************************
//    TPaletteColumnFx  implementation
//****************************************************************************************

TPaletteColumnFx::TPaletteColumnFx() : m_paletteColumn(0) {}

//-------------------------------------------------------------------

TPaletteColumnFx::~TPaletteColumnFx() {}

//--------------------------------------------------

TFx *TPaletteColumnFx::clone(bool recursive) const {
  TPaletteColumnFx *clonedFx =
      dynamic_cast<TPaletteColumnFx *>(TFx::clone(recursive));
  assert(clonedFx);
  clonedFx->m_paletteColumn = m_paletteColumn;
  return clonedFx;
}

//-------------------------------------------------------------------

void TPaletteColumnFx::doCompute(TTile &tile, double frame,
                                 const TRenderSettings &) {}

//-------------------------------------------------------------------

bool TPaletteColumnFx::doGetBBox(double frame, TRectD &bBox,
                                 const TRenderSettings &info) {
  return false;
}

//-------------------------------------------------------------------

TAffine TPaletteColumnFx::getDpiAff(int frame) { return TAffine(); }

//-------------------------------------------------------------------

bool TPaletteColumnFx::canHandle(const TRenderSettings &info, double frame) {
  return false;
}

//-------------------------------------------------------------------

TFilePath TPaletteColumnFx::getPalettePath(int frame) const {
  if (!m_paletteColumn) return TFilePath();
  TXshCell cell = m_paletteColumn->getCell(frame);
  if (cell.isEmpty() || cell.m_level->getPaletteLevel() == 0)
    return TFilePath();

  TXshPaletteLevel *paletteLevel = cell.m_level->getPaletteLevel();
  TFilePath path                 = paletteLevel->getPath();
  path = paletteLevel->getScene()->decodeFilePath(path);
  return path;
}

//-------------------------------------------------------------------

TPalette *TPaletteColumnFx::getPalette(int frame) const {
  if (!m_paletteColumn) return 0;
  TXshCell cell = m_paletteColumn->getCell(frame);
  if (cell.isEmpty() || cell.m_level->getPaletteLevel() == 0) return 0;

  TXshPaletteLevel *paletteLevel = cell.m_level->getPaletteLevel();
  return paletteLevel->getPalette();
}

//-------------------------------------------------------------------

const TPersistDeclaration *TPaletteColumnFx::getDeclaration() const {
  return &paletteColumnFxInfo;
}

//-------------------------------------------------------------------

std::string TPaletteColumnFx::getPluginId() const { return "Toonz_"; }

//-------------------------------------------------------------------

TFxTimeRegion TPaletteColumnFx::getTimeRegion() const {
  int first = 0;
  int last  = 11;
  return TFxTimeRegion(first, last);
}

//-------------------------------------------------------------------

std::wstring TPaletteColumnFx::getColumnName() const {
  if (!m_paletteColumn) return L"Col?";
  return L"Col" + std::to_wstring(m_paletteColumn->getIndex() + 1);
}

//-------------------------------------------------------------------

std::wstring TPaletteColumnFx::getColumnId() const {
  if (!m_paletteColumn) return L"Col?";
  return L"Col" + std::to_wstring(m_paletteColumn->getIndex() + 1);
}

//-------------------------------------------------------------------

std::string TPaletteColumnFx::getAlias(double frame,
                                       const TRenderSettings &info) const {
  TFilePath palettePath = getPalettePath(frame);
  return "TPaletteColumnFx[" + ::to_string(palettePath.getWideString()) + "]";
}

//-------------------------------------------------------------------

int TPaletteColumnFx::getColumnIndex() const {
  return m_paletteColumn ? m_paletteColumn->getIndex() : -1;
}

//-------------------------------------------------------------------

TXshColumn *TPaletteColumnFx::getXshColumn() const { return m_paletteColumn; }

//****************************************************************************************
//    TZeraryColumnFx  implementation
//****************************************************************************************

TZeraryColumnFx::TZeraryColumnFx() : m_zeraryFxColumn(0), m_fx(0) {
  setName(L"ZeraryColumn");
}

//-------------------------------------------------------------------

TZeraryColumnFx::~TZeraryColumnFx() {
  if (m_zeraryFxColumn) m_zeraryFxColumn->release();
  if (m_fx) {
    m_fx->m_columnFx = 0;
    m_fx->release();
  }
}

//-------------------------------------------------------------------

void TZeraryColumnFx::doCompute(TTile &tile, double frame,
                                const TRenderSettings &info) {
  if (m_zeraryFxColumn) {
    TRasterFx *fx = dynamic_cast<TRasterFx *>(m_fx);
    if (fx) fx->compute(tile, frame, info);
  }
}

//-------------------------------------------------------------------

bool TZeraryColumnFx::doGetBBox(double frame, TRectD &bBox,
                                const TRenderSettings &info) {
  if (m_zeraryFxColumn) {
    TRasterFx *fx = dynamic_cast<TRasterFx *>(m_fx);
    if (fx) return fx->doGetBBox(frame, bBox, info);
  }

  return false;
}

//-------------------------------------------------------------------

const TPersistDeclaration *TZeraryColumnFx::getDeclaration() const {
  return &zeraryColumnFxInfo;
}

//-------------------------------------------------------------------

std::string TZeraryColumnFx::getPluginId() const { return "Toonz_"; }

//-------------------------------------------------------------------

TFxTimeRegion TZeraryColumnFx::getTimeRegion() const {
  return TFxTimeRegion(0, (std::numeric_limits<double>::max)());
}

//-------------------------------------------------------------------

void TZeraryColumnFx::setColumn(TXshZeraryFxColumn *column) {
  m_zeraryFxColumn = column;
}

//-------------------------------------------------------------------

std::wstring TZeraryColumnFx::getColumnName() const {
  return getZeraryFx()->getName();
}

//-------------------------------------------------------------------

std::wstring TZeraryColumnFx::getColumnId() const {
  return getZeraryFx()->getFxId();
}

//-------------------------------------------------------------------

void TZeraryColumnFx::setZeraryFx(TFx *fx) {
  TZeraryFx *zfx = static_cast<TZeraryFx *>(fx);

  if (fx) {
    assert(dynamic_cast<TZeraryFx *>(fx));

    fx->addRef();
    fx->setNewIdentifier();  // When adopting zerary fxs, ensure that they
                             // have a new numeric identifier
    zfx->m_columnFx = this;
  }

  if (m_fx) {
    m_fx->m_columnFx = 0;
    m_fx->release();
  }

  m_fx = zfx;
}

//-------------------------------------------------------------------

std::string TZeraryColumnFx::getAlias(double frame,
                                      const TRenderSettings &info) const {
  return "TZeraryColumnFx[" + m_fx->getAlias(frame, info) + "]";
}

//-------------------------------------------------------------------

void TZeraryColumnFx::loadData(TIStream &is) {
  if (m_fx) m_fx->release();

  m_fx        = 0;
  TPersist *p = 0;
  is >> p;

  m_fx = dynamic_cast<TZeraryFx *>(p);
  if (m_fx) {
    m_fx->addRef();
    m_fx->m_columnFx = this;
    m_fx->setNewIdentifier();
  }

  TFx::loadData(is);
  setNewIdentifier();  // Same as with TColumnFx
}

//-------------------------------------------------------------------

void TZeraryColumnFx::saveData(TOStream &os) {
  assert(m_fx);
  os << m_fx;
  TFx::saveData(os);
}

//-------------------------------------------------------------------

int TZeraryColumnFx::getColumnIndex() const {
  return m_zeraryFxColumn ? m_zeraryFxColumn->getIndex() : -1;
}

//-------------------------------------------------------------------

TXshColumn *TZeraryColumnFx::getXshColumn() const { return m_zeraryFxColumn; }

//****************************************************************************************
//    TXSheetFx  implementation
//****************************************************************************************

const TPersistDeclaration *TXsheetFx::getDeclaration() const {
  return &infoTXsheetFx;
}

//-------------------------------------------------------------------

TXsheetFx::TXsheetFx() : m_fxDag(0) { setName(L"Xsheet"); }

//-------------------------------------------------------------------

void TXsheetFx::doCompute(TTile &tile, double frame, const TRenderSettings &) {}

//-------------------------------------------------------------------

bool TXsheetFx::doGetBBox(double frame, TRectD &bBox,
                          const TRenderSettings &info) {
  return false;
}

//-------------------------------------------------------------------

std::string TXsheetFx::getPluginId() const { return "Toonz_"; }

//-------------------------------------------------------------------

std::string TXsheetFx::getAlias(double frame,
                                const TRenderSettings &info) const {
  std::string alias = getFxType();
  alias += "[";

  // Add each terminal fx's alias
  TFxSet *terminalFxs = m_fxDag->getTerminalFxs();
  int i, fxsCount = terminalFxs->getFxCount();
  for (i = 0; i < fxsCount; ++i) {
    alias +=
        static_cast<TRasterFx *>(terminalFxs->getFx(i))->getAlias(frame, info) +
        ",";
  }

  return alias + "]";
}

//-------------------------------------------------------------------

void TXsheetFx::setFxDag(FxDag *fxDag) { m_fxDag = fxDag; }

//****************************************************************************************
//    TOutputFx  implementation
//****************************************************************************************

TOutputFx::TOutputFx() {
  addInputPort("source", m_input);
  setName(L"Output");
}

//-------------------------------------------------------------------

const TPersistDeclaration *TOutputFx::getDeclaration() const {
  return &infoTOutputFx;
}

//-------------------------------------------------------------------

void TOutputFx::doCompute(TTile &tile, double frame, const TRenderSettings &) {}

//-------------------------------------------------------------------

bool TOutputFx::doGetBBox(double frame, TRectD &bBox,
                          const TRenderSettings &info) {
  return false;
}

//-------------------------------------------------------------------

std::string TOutputFx::getPluginId() const { return "Toonz_"; }