1 /* ***** BEGIN LICENSE BLOCK *****
2  * This file is part of openfx-misc <https://github.com/devernay/openfx-misc>,
3  * Copyright (C) 2013-2018 INRIA
4  *
5  * openfx-misc is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * openfx-misc is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with openfx-misc.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>
17  * ***** END LICENSE BLOCK ***** */
18 
19 /*
20  * OFX Crop plugin.
21  */
22 
23 #include <cmath>
24 #include <cfloat> // DBL_MAX
25 #include <algorithm>
26 
27 #include "ofxsProcessing.H"
28 #include "ofxsCoords.h"
29 #include "ofxsRectangleInteract.h"
30 #include "ofxsMacros.h"
31 #include "ofxsGenerator.h"
32 #include "ofxsFormatResolution.h"
33 #include "ofxsThreadSuite.h"
34 
35 using namespace OFX;
36 
37 OFXS_NAMESPACE_ANONYMOUS_ENTER
38 
39 #define kPluginName "CropOFX"
40 #define kPluginGrouping "Transform"
41 #define kPluginDescription "Removes everything outside the defined rectangle and optionally adds black edges so everything outside is black.\n" \
42     "If the 'Extent' parameter is set to 'Format', and 'Reformat' is checked, the output pixel aspect ratio is also set to this of the format.\n" \
43     "This plugin does not concatenate transforms."
44 #define kPluginIdentifier "net.sf.openfx.CropPlugin"
45 #define kPluginVersionMajor 1 // Incrementing this number means that you have broken backwards compatibility of the plug-in.
46 #define kPluginVersionMinor 0 // Increment this when you have fixed a bug or made it faster.
47 
48 #define kSupportsTiles 1
49 #define kSupportsMultiResolution 1
50 #define kSupportsRenderScale 1
51 #define kSupportsMultipleClipPARs false
52 #define kSupportsMultipleClipDepths false
53 #define kRenderThreadSafety eRenderFullySafe
54 
55 #define kParamReformat "reformat"
56 #define kParamReformatLabel "Reformat"
57 #define kParamReformatHint "Translates the bottom left corner of the crop rectangle to be in (0,0)."
58 #define kParamReformatHintExtraNatron " This sets the output format only if 'Format' or 'Project' is selected as the output Extend. In order to actually change the format of this image stream for other Extent choices, feed the output of this node to a either a NoOp node which sets the proper format, or a Reformat node with the same extent and with 'Resize Type' set to None and 'Center' unchecked. The reason is that the Crop size may be animated, but the output format can not be animated."
59 #define kParamReformatDefault false
60 
61 #define kParamIntersect "intersect"
62 #define kParamIntersectLabel "Intersect"
63 #define kParamIntersectHint "Intersects the crop rectangle with the input region of definition instead of extending it."
64 
65 #define kParamBlackOutside "blackOutside"
66 #define kParamBlackOutsideLabel "Black Outside"
67 #define kParamBlackOutsideHint "Add 1 black and transparent pixel to the region of definition so that all the area outside the crop rectangle is black."
68 
69 #define kParamSoftness "softness"
70 #define kParamSoftnessLabel "Softness"
71 #define kParamSoftnessHint "Size of the fade to black around edges to apply."
72 
73 static inline
74 double
rampSmooth(double t)75 rampSmooth(double t)
76 {
77     t *= 2.;
78     if (t < 1) {
79         return t * t / (2.);
80     } else {
81         t -= 1.;
82 
83         return -0.5 * (t * (t - 2) - 1);
84     }
85 }
86 
87 class CropProcessorBase
88     : public ImageProcessor
89 {
90 protected:
91     const Image *_srcImg;
92     OfxRectD _cropRect;
93     OfxRectD _cropRectFull;
94     OfxPointD _renderScale;
95     double _par;
96     OfxPointI _translation;
97     bool _blackOutside;
98     double _softness;
99     OfxRectI _cropRectPixel;
100     OfxRectI _cropRectFullPixel;
101 
102 public:
CropProcessorBase(ImageEffect & instance)103     CropProcessorBase(ImageEffect &instance)
104         : ImageProcessor(instance)
105         , _srcImg(NULL)
106         , _par(1.)
107         , _blackOutside(false)
108         , _softness(0.)
109     {
110         _cropRect.x1 = _cropRect.y1 = _cropRect.x2 = _cropRect.y2 = 0.;
111         _cropRectFull.x1 = _cropRectFull.y1 = _cropRectFull.x2 = _cropRectFull.y2 = 0.;
112         _renderScale.x = _renderScale.y = 1.;
113         _cropRectPixel.x1 = _cropRectPixel.y1 = _cropRectPixel.x2 = _cropRectPixel.y2 = 0;
114         _cropRectFullPixel.x1 = _cropRectFullPixel.y1 = _cropRectFullPixel.x2 = _cropRectFullPixel.y2 = 0;
115         _translation.x = _translation.y = 0;
116     }
117 
118     /** @brief set the src image */
setSrcImg(const Image * v)119     void setSrcImg(const Image *v)
120     {
121         _srcImg = v;
122     }
123 
setValues(const OfxRectD & cropRect,const OfxRectD & cropRectFull,const OfxPointD & renderScale,double par,bool blackOutside,bool reformat,double softness)124     void setValues(const OfxRectD& cropRect,
125                    const OfxRectD& cropRectFull, // without intersection
126                    const OfxPointD& renderScale,
127                    double par,
128                    bool blackOutside,
129                    bool reformat,
130                    double softness)
131     {
132         _cropRect = cropRect;
133         _cropRectFull = cropRectFull;
134         _renderScale = renderScale;
135         _par = par;
136         _blackOutside = blackOutside;
137         _softness = softness;
138         Coords::toPixelNearest(cropRect, renderScale, par, &_cropRectPixel);
139         Coords::toPixelNearest(cropRectFull, renderScale, par, &_cropRectFullPixel);
140         if (reformat) {
141             _translation.x = -_cropRectFullPixel.x1;
142             _translation.y = -_cropRectFullPixel.y1;
143         } else {
144             _translation.x = 0;
145             _translation.y = 0;
146         }
147     }
148 };
149 
150 
151 template <class PIX, int nComponents, int maxValue>
152 class CropProcessor
153     : public CropProcessorBase
154 {
155 public:
CropProcessor(ImageEffect & instance)156     CropProcessor(ImageEffect &instance)
157         : CropProcessorBase(instance)
158     {
159     }
160 
161 private:
multiThreadProcessImages(OfxRectI procWindow)162     void multiThreadProcessImages(OfxRectI procWindow)
163     {
164         //assert(filter == _filter);
165         for (int y = procWindow.y1; y < procWindow.y2; ++y) {
166             if ( _effect.abort() ) {
167                 break;
168             }
169 
170             PIX *dstPix = (PIX *) _dstImg->getPixelAddress(procWindow.x1, y);
171             bool yblack = _blackOutside && ( y == (_cropRectFullPixel.y1 + _translation.y) || y == (_cropRectFullPixel.y2 - 1 + _translation.y) );
172 
173             for (int x = procWindow.x1; x < procWindow.x2; ++x, dstPix += nComponents) {
174                 bool xblack = _blackOutside && ( x == (_cropRectFullPixel.x1 + _translation.x) || x == (_cropRectFullPixel.x2 - 1 + _translation.x) );
175                 // treat the black case separately
176                 if (xblack || yblack || !_srcImg) {
177                     for (int k = 0; k < nComponents; ++k) {
178                         dstPix[k] =  PIX();
179                     }
180                 } else {
181                     OfxPointI p_pixel;
182                     OfxPointD p;
183                     p_pixel.x = x - _translation.x;
184                     p_pixel.y = y - _translation.y;
185                     Coords::toCanonical(p_pixel, _dstImg->getRenderScale(), _dstImg->getPixelAspectRatio(), &p);
186                     double dx = std::min(p.x - _cropRectFull.x1, _cropRectFull.x2 - p.x);
187                     double dy = std::min(p.y - _cropRectFull.y1, _cropRectFull.y2 - p.y);
188 
189                     if ( _blackOutside && ( (dx <= 0) || (dy <= 0) ) ) {
190                         // outside of the rectangle
191                         for (int k = 0; k < nComponents; ++k) {
192                             dstPix[k] =  PIX();
193                         }
194                     } else {
195                         const PIX *srcPix = (const PIX*)_srcImg->getPixelAddressNearest(p_pixel.x, p_pixel.y);
196                         //if (!srcPix) {
197                         //    for (int k = 0; k < nComponents; ++k) {
198                         //        dstPix[k] =  PIX();
199                         //    }
200                         //} else
201                         if ( (_softness == 0) || ( (dx >= _softness) && (dy >= _softness) ) ) {
202                             // inside of the rectangle
203                             for (int k = 0; k < nComponents; ++k) {
204                                 dstPix[k] =  srcPix[k];
205                             }
206                         } else {
207                             double tx, ty;
208                             if (dx >= _softness) {
209                                 tx = 1.;
210                             } else {
211                                 tx = rampSmooth(dx / _softness);
212                             }
213                             if (dy >= _softness) {
214                                 ty = 1.;
215                             } else {
216                                 ty = rampSmooth(dy / _softness);
217                             }
218                             double t = tx * ty;
219                             if (t >= 1) {
220                                 for (int k = 0; k < nComponents; ++k) {
221                                     dstPix[k] =  srcPix[k];
222                                 }
223                             } else {
224                                 //if (_plinear) {
225                                 //    // it seems to be the way Nuke does it... I could understand t*t, but why t*t*t?
226                                 //    t = t*t*t;
227                                 //}
228                                 for (int k = 0; k < nComponents; ++k) {
229                                     dstPix[k] =  PIX(srcPix[k] * t);
230                                 }
231                             }
232                         }
233                     }
234                 }
235             }
236         }
237     } // multiThreadProcessImages
238 };
239 
240 
241 ////////////////////////////////////////////////////////////////////////////////
242 /** @brief The plugin that does our work */
243 class CropPlugin
244     : public ImageEffect
245 {
246 public:
247     /** @brief ctor */
CropPlugin(OfxImageEffectHandle handle)248     CropPlugin(OfxImageEffectHandle handle)
249         : ImageEffect(handle)
250         , _dstClip(NULL)
251         , _srcClip(NULL)
252         , _extent(NULL)
253         , _format(NULL)
254         , _formatSize(NULL)
255         , _formatPar(NULL)
256         , _btmLeft(NULL)
257         , _size(NULL)
258         , _interactive(NULL)
259         , _recenter(NULL)
260         , _softness(NULL)
261         , _reformat(NULL)
262         , _intersect(NULL)
263         , _blackOutside(NULL)
264     {
265         _dstClip = fetchClip(kOfxImageEffectOutputClipName);
266         assert( _dstClip && (!_dstClip->isConnected() || _dstClip->getPixelComponents() == ePixelComponentAlpha ||
267                              _dstClip->getPixelComponents() == ePixelComponentRGB ||
268                              _dstClip->getPixelComponents() == ePixelComponentRGBA) );
269         _srcClip = getContext() == eContextGenerator ? NULL : fetchClip(kOfxImageEffectSimpleSourceClipName);
270         assert( (!_srcClip && getContext() == eContextGenerator) ||
271                 ( _srcClip && (!_srcClip->isConnected() || _srcClip->getPixelComponents() ==  ePixelComponentAlpha ||
272                                _srcClip->getPixelComponents() == ePixelComponentRGB ||
273                                _srcClip->getPixelComponents() == ePixelComponentRGBA) ) );
274 
275         _rectangleInteractEnable = fetchBooleanParam(kParamRectangleInteractEnable);
276         _extent = fetchChoiceParam(kParamGeneratorExtent);
277         _format = fetchChoiceParam(kParamGeneratorFormat);
278         _formatSize = fetchInt2DParam(kParamGeneratorSize);
279         _formatPar = fetchDoubleParam(kParamGeneratorPAR);
280         _btmLeft = fetchDouble2DParam(kParamRectangleInteractBtmLeft);
281         _size = fetchDouble2DParam(kParamRectangleInteractSize);
282         _recenter = fetchPushButtonParam(kParamGeneratorCenter);
283         _interactive = fetchBooleanParam(kParamRectangleInteractInteractive);
284         _softness = fetchDoubleParam(kParamSoftness);
285         _reformat = fetchBooleanParam(kParamReformat);
286         _intersect = fetchBooleanParam(kParamIntersect);
287         _blackOutside = fetchBooleanParam(kParamBlackOutside);
288 
289         assert(_rectangleInteractEnable && _btmLeft && _size && _softness && _reformat && _intersect && _blackOutside);
290 
291         // finally
292         syncPrivateData();
293     }
294 
295 private:
296     // override the roi call
297     virtual void getRegionsOfInterest(const RegionsOfInterestArguments &args, RegionOfInterestSetter &rois) OVERRIDE FINAL;
298     virtual bool getRegionOfDefinition(const RegionOfDefinitionArguments &args, OfxRectD &rod) OVERRIDE FINAL;
299 
300     /* Override the render */
301     virtual void render(const RenderArguments &args) OVERRIDE FINAL;
302 
303     /** @brief The sync private data action, called when the effect needs to sync any private data to persistent parameters */
syncPrivateData(void)304     virtual void syncPrivateData(void) OVERRIDE FINAL
305     {
306         updateParamsVisibility();
307     }
308 
309     template <int nComponents>
310     void renderInternal(const RenderArguments &args, BitDepthEnum dstBitDepth);
311 
312     /* set up and run a processor */
313     void setupAndProcess(CropProcessorBase &, const RenderArguments &args);
314 
315     virtual void changedParam(const InstanceChangedArgs &args, const std::string &paramName) OVERRIDE FINAL;
316     virtual void getClipPreferences(ClipPreferencesSetter &clipPreferences) OVERRIDE FINAL;
317 
318     void getCropRectangle(OfxTime time,
319                           const OfxPointD& renderScale,
320                           bool useIntersect,
321                           bool forceIntersect,
322                           bool useBlackOutside,
323                           bool useReformat,
324                           OfxRectD* cropRect,
325                           double* par) const;
326 
327     void updateParamsVisibility();
328 
getSrcClip() const329     Clip* getSrcClip() const
330     {
331         return _srcClip;
332     }
333 
334 private:
335     // do not need to delete these, the ImageEffect is managing them for us
336     Clip *_dstClip;
337     Clip *_srcClip;
338     BooleanParam* _rectangleInteractEnable;
339     ChoiceParam* _extent;
340     ChoiceParam* _format;
341     Int2DParam* _formatSize;
342     DoubleParam* _formatPar;
343     Double2DParam* _btmLeft;
344     Double2DParam* _size;
345     BooleanParam* _interactive;
346     PushButtonParam *_recenter;
347     DoubleParam* _softness;
348     BooleanParam* _reformat;
349     BooleanParam* _intersect;
350     BooleanParam* _blackOutside;
351 };
352 
353 void
getCropRectangle(OfxTime time,const OfxPointD & renderScale,bool useIntersect,bool forceIntersect,bool useBlackOutside,bool useReformat,OfxRectD * cropRect,double * pixelAspectRatio) const354 CropPlugin::getCropRectangle(OfxTime time,
355                              const OfxPointD& renderScale,
356                              bool useIntersect,
357                              bool forceIntersect,
358                              bool useBlackOutside,
359                              bool useReformat,
360                              OfxRectD* cropRect,
361                              double* pixelAspectRatio) const
362 {
363     bool intersect = false;
364 
365     if (useIntersect) {
366         if (!forceIntersect) {
367             intersect = _intersect->getValueAtTime(time);
368         } else {
369             intersect = true;
370         }
371     }
372 
373     bool blackOutside = false;
374     if (useBlackOutside) {
375         blackOutside = _blackOutside->getValueAtTime(time);
376     }
377 
378     bool reformat = false;
379     if (useReformat) {
380         reformat = _reformat->getValueAtTime(time);
381     }
382 
383     OfxRectD rod;
384     double par;
385 
386     // below: see GeneratorPlugin::getRegionOfDefinition
387     GeneratorExtentEnum extent = (GeneratorExtentEnum)_extent->getValue();
388 
389     switch (extent) {
390     case eGeneratorExtentFormat: {
391         OfxRectI pixelFormat;
392         int w, h;
393         _formatSize->getValue(w, h);
394         _formatPar->getValue(par);
395         pixelFormat.x1 = pixelFormat.y1 = 0;
396         pixelFormat.x2 = w;
397         pixelFormat.y2 = h;
398         const OfxPointD rsOne = {1., 1.};
399         Coords::toCanonical(pixelFormat, rsOne, par, &rod);
400         break;
401     }
402     case eGeneratorExtentSize: {
403         par = _srcClip->getPixelAspectRatio();
404         _size->getValueAtTime(time, rod.x2, rod.y2);
405         _btmLeft->getValue(rod.x1, rod.y1);
406         rod.x2 += rod.x1;
407         rod.y2 += rod.y1;
408         break;
409     }
410     case eGeneratorExtentProject: {
411         OfxPointD siz = getProjectSize();
412         OfxPointD off = getProjectOffset();
413         rod.x1 = off.x;
414         rod.x2 = off.x + siz.x;
415         rod.y1 = off.y;
416         rod.y2 = off.y + siz.y;
417         par = getProjectPixelAspectRatio();
418         break;
419     }
420     case eGeneratorExtentDefault: {
421         if ( _srcClip->isConnected() ) {
422             rod = _srcClip->getRegionOfDefinition(time);
423             par = _srcClip->getPixelAspectRatio();
424         } else {
425             OfxPointD siz = getProjectSize();
426             OfxPointD off = getProjectOffset();
427             rod.x1 = off.x;
428             rod.x2 = off.x + siz.x;
429             rod.y1 = off.y;
430             rod.y2 = off.y + siz.y;
431             par = getProjectPixelAspectRatio();
432         }
433         break;
434     }
435     }
436 
437     if (reformat) {
438         rod.x2 -= rod.x1;
439         rod.y2 -= rod.y1;
440         rod.x1 = 0.;
441         rod.y1 = 0.;
442     }
443     if (intersect && _srcClip) {
444         const OfxRectD& srcRoD = _srcClip->getRegionOfDefinition(time);
445         Coords::rectIntersection(rod, srcRoD, &rod);
446     }
447 
448     if (blackOutside) {
449         OfxRectI rodPixel;
450         Coords::toPixelEnclosing(rod, renderScale, par, &rodPixel);
451         rodPixel.x1 -= 1;
452         rodPixel.y1 -= 1;
453         rodPixel.x2 += 1;
454         rodPixel.y2 += 1;
455         Coords::toCanonical(rodPixel, renderScale, par, &rod);
456     }
457 
458     if (cropRect) {
459         *cropRect = rod;
460     }
461     if (pixelAspectRatio) {
462         *pixelAspectRatio = par;
463     }
464 } // CropPlugin::getCropRectangle
465 
466 ////////////////////////////////////////////////////////////////////////////////
467 /** @brief render for the filter */
468 
469 ////////////////////////////////////////////////////////////////////////////////
470 // basic plugin render function, just a skelington to instantiate templates from
471 
472 /* set up and run a processor */
473 void
setupAndProcess(CropProcessorBase & processor,const RenderArguments & args)474 CropPlugin::setupAndProcess(CropProcessorBase &processor,
475                             const RenderArguments &args)
476 {
477     const double time = args.time;
478 
479     auto_ptr<Image> dst( _dstClip->fetchImage(args.time) );
480 
481     if ( !dst.get() ) {
482         throwSuiteStatusException(kOfxStatFailed);
483     }
484     BitDepthEnum dstBitDepth    = dst->getPixelDepth();
485     PixelComponentEnum dstComponents  = dst->getPixelComponents();
486     if ( ( dstBitDepth != _dstClip->getPixelDepth() ) ||
487          ( dstComponents != _dstClip->getPixelComponents() ) ) {
488         setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong depth or components");
489         throwSuiteStatusException(kOfxStatFailed);
490     }
491     if ( (dst->getRenderScale().x != args.renderScale.x) ||
492          ( dst->getRenderScale().y != args.renderScale.y) ||
493          ( ( dst->getField() != eFieldNone) /* for DaVinci Resolve */ && ( dst->getField() != args.fieldToRender) ) ) {
494         setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
495         throwSuiteStatusException(kOfxStatFailed);
496     }
497     auto_ptr<const Image> src( ( _srcClip && _srcClip->isConnected() ) ?
498                                     _srcClip->fetchImage(args.time) : 0 );
499     if ( src.get() ) {
500         if ( (src->getRenderScale().x != args.renderScale.x) ||
501              ( src->getRenderScale().y != args.renderScale.y) ||
502              ( ( src->getField() != eFieldNone) /* for DaVinci Resolve */ && ( src->getField() != args.fieldToRender) ) ) {
503             setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
504             throwSuiteStatusException(kOfxStatFailed);
505         }
506         BitDepthEnum dstBitDepth       = dst->getPixelDepth();
507         PixelComponentEnum dstComponents  = dst->getPixelComponents();
508         BitDepthEnum srcBitDepth      = src->getPixelDepth();
509         PixelComponentEnum srcComponents = src->getPixelComponents();
510         if ( (srcBitDepth != dstBitDepth) || (srcComponents != dstComponents) ) {
511             throwSuiteStatusException(kOfxStatFailed);
512         }
513     }
514 
515     // set the images
516     processor.setDstImg( dst.get() );
517     processor.setSrcImg( src.get() );
518 
519     // set the render window
520     processor.setRenderWindow(args.renderWindow);
521 
522     bool reformat = _reformat->getValueAtTime(args.time);
523     bool blackOutside = _blackOutside->getValueAtTime(args.time);
524     OfxRectD cropRectCanonical;
525     OfxRectD cropRectFullCanonical;
526     double par;
527     getCropRectangle(time, args.renderScale, /*useIntersect=*/ true, /*forceIntersect=*/ false, /*useBlackOutside=*/ false, /*useReformat=*/ false, &cropRectCanonical, &par);
528     getCropRectangle(time, args.renderScale, /*useIntersect=*/ false, /*forceIntersect=*/ false, /*useBlackOutside=*/ false, /*useReformat=*/ false, &cropRectFullCanonical, &par);
529     double softness = _softness->getValueAtTime(args.time);
530     // no need to softness *= args.renderScale.x; since softness is computed on canonical coords
531 
532     processor.setValues(cropRectCanonical, cropRectFullCanonical, args.renderScale, par, blackOutside, reformat, softness);
533 
534     // Call the base class process member, this will call the derived templated process code
535     processor.process();
536 } // CropPlugin::setupAndProcess
537 
538 // override the roi call
539 // Required if the plugin requires a region from the inputs which is different from the rendered region of the output.
540 // (this is the case here)
541 void
getRegionsOfInterest(const RegionsOfInterestArguments & args,RegionOfInterestSetter & rois)542 CropPlugin::getRegionsOfInterest(const RegionsOfInterestArguments &args,
543                                  RegionOfInterestSetter &rois)
544 {
545     OfxRectD cropRect;
546 
547     getCropRectangle(args.time, args.renderScale, /*useIntersect=*/ true, /*forceIntersect=*/ true, /*useBlackOutside=*/ false, /*useReformat=*/ false, &cropRect, NULL);
548 
549     OfxRectD roi = args.regionOfInterest;
550     bool reformat = _reformat->getValueAtTime(args.time);
551     if (reformat) {
552         // translate, because cropRect will be rendered at (0,0) in this case
553         // Remember: this is the region of INTEREST: the region from the input
554         // used to render the region args.regionOfInterest
555         roi.x1 += cropRect.x1;
556         roi.y1 += cropRect.y1;
557         roi.x2 += cropRect.x1;
558         roi.y2 += cropRect.y1;
559     }
560 
561     // intersect the crop rectangle with args.regionOfInterest
562     Coords::rectIntersection(cropRect, roi, &cropRect);
563     rois.setRegionOfInterest(*_srcClip, cropRect);
564 }
565 
566 bool
getRegionOfDefinition(const RegionOfDefinitionArguments & args,OfxRectD & rod)567 CropPlugin::getRegionOfDefinition(const RegionOfDefinitionArguments &args,
568                                   OfxRectD &rod)
569 {
570     getCropRectangle(args.time, args.renderScale, /*useIntersect=*/ true, /*forceIntersect=*/ false, /*useBlackOutside=*/ true, /*useReformat=*/ true, &rod, NULL);
571 
572     return true;
573 }
574 
575 // the internal render function
576 template <int nComponents>
577 void
renderInternal(const RenderArguments & args,BitDepthEnum dstBitDepth)578 CropPlugin::renderInternal(const RenderArguments &args,
579                            BitDepthEnum dstBitDepth)
580 {
581     switch (dstBitDepth) {
582     case eBitDepthUByte: {
583         CropProcessor<unsigned char, nComponents, 255> fred(*this);
584         setupAndProcess(fred, args);
585         break;
586     }
587     case eBitDepthUShort: {
588         CropProcessor<unsigned short, nComponents, 65535> fred(*this);
589         setupAndProcess(fred, args);
590         break;
591     }
592     case eBitDepthFloat: {
593         CropProcessor<float, nComponents, 1> fred(*this);
594         setupAndProcess(fred, args);
595         break;
596     }
597     default:
598         throwSuiteStatusException(kOfxStatErrUnsupported);
599     }
600 }
601 
602 // the overridden render function
603 void
render(const RenderArguments & args)604 CropPlugin::render(const RenderArguments &args)
605 {
606     // instantiate the render code based on the pixel depth of the dst clip
607     BitDepthEnum dstBitDepth    = _dstClip->getPixelDepth();
608     PixelComponentEnum dstComponents  = _dstClip->getPixelComponents();
609 
610     assert( kSupportsMultipleClipPARs   || !_srcClip || _srcClip->getPixelAspectRatio() == _dstClip->getPixelAspectRatio() );
611     assert( kSupportsMultipleClipDepths || !_srcClip || _srcClip->getPixelDepth()       == _dstClip->getPixelDepth() );
612     assert(dstComponents == ePixelComponentRGBA || dstComponents == ePixelComponentRGB || dstComponents == ePixelComponentXY || dstComponents == ePixelComponentAlpha);
613     if (dstComponents == ePixelComponentRGBA) {
614         renderInternal<4>(args, dstBitDepth);
615     } else if (dstComponents == ePixelComponentRGB) {
616         renderInternal<3>(args, dstBitDepth);
617 #ifdef OFX_EXTENSIONS_NATRON
618     } else if (dstComponents == ePixelComponentXY) {
619         renderInternal<2>(args, dstBitDepth);
620 #endif
621     } else {
622         assert(dstComponents == ePixelComponentAlpha);
623         renderInternal<1>(args, dstBitDepth);
624     }
625 }
626 
627 void
updateParamsVisibility()628 CropPlugin::updateParamsVisibility()
629 {
630     GeneratorExtentEnum extent = (GeneratorExtentEnum)_extent->getValue();
631     bool hasFormat = (extent == eGeneratorExtentFormat);
632     bool hasSize = (extent == eGeneratorExtentSize);
633 
634     _format->setIsSecretAndDisabled(!hasFormat);
635     _size->setIsSecretAndDisabled(!hasSize);
636     _recenter->setIsSecretAndDisabled(!hasSize);
637     _btmLeft->setIsSecretAndDisabled(!hasSize);
638     _interactive->setIsSecretAndDisabled(!hasSize);
639 }
640 
641 void
changedParam(const InstanceChangedArgs & args,const std::string & paramName)642 CropPlugin::changedParam(const InstanceChangedArgs &args,
643                          const std::string &paramName)
644 {
645     const double time = args.time;
646 
647     if (paramName == kParamReformat) {
648         bool reformat;
649         _reformat->getValueAtTime(args.time, reformat);
650         _rectangleInteractEnable->setValue(!reformat);
651         if (args.reason == eChangeUserEdit) {
652             _blackOutside->setValue(!reformat); // disable black outside when reformat is checked and vice-versa
653         }
654     } else if ( (paramName == kParamGeneratorExtent) && (args.reason == eChangeUserEdit) ) {
655         updateParamsVisibility();
656     } else if (paramName == kParamGeneratorFormat) {
657         //the host does not handle the format itself, do it ourselves
658         EParamFormat format = (EParamFormat)_format->getValue();
659         int w = 0, h = 0;
660         double par = -1;
661         getFormatResolution(format, &w, &h, &par);
662         assert(par != -1);
663         _formatPar->setValue(par);
664         _formatSize->setValue(w, h);
665     } else if (paramName == kParamGeneratorCenter) {
666         Clip* srcClip = getSrcClip();
667         OfxRectD srcRoD;
668         if ( srcClip && srcClip->isConnected() ) {
669             srcRoD = srcClip->getRegionOfDefinition(args.time);
670         } else {
671             OfxPointD siz = getProjectSize();
672             OfxPointD off = getProjectOffset();
673             srcRoD.x1 = off.x;
674             srcRoD.x2 = off.x + siz.x;
675             srcRoD.y1 = off.y;
676             srcRoD.y2 = off.y + siz.y;
677         }
678         OfxPointD center;
679         center.x = (srcRoD.x2 + srcRoD.x1) / 2.;
680         center.y = (srcRoD.y2 + srcRoD.y1) / 2.;
681 
682         OfxRectD rectangle;
683         _size->getValueAtTime(time, rectangle.x2, rectangle.y2);
684         _btmLeft->getValueAtTime(time, rectangle.x1, rectangle.y1);
685         rectangle.x2 += rectangle.x1;
686         rectangle.y2 += rectangle.y1;
687 
688         OfxRectD newRectangle;
689         newRectangle.x1 = center.x - (rectangle.x2 - rectangle.x1) / 2.;
690         newRectangle.y1 = center.y - (rectangle.y2 - rectangle.y1) / 2.;
691         newRectangle.x2 = newRectangle.x1 + (rectangle.x2 - rectangle.x1);
692         newRectangle.y2 = newRectangle.y1 + (rectangle.y2 - rectangle.y1);
693 
694         _size->setValue(newRectangle.x2 - newRectangle.x1, newRectangle.y2 - newRectangle.y1);
695         _btmLeft->setValue(newRectangle.x1, newRectangle.y1);
696     }
697 } // CropPlugin::changedParam
698 
699 void
getClipPreferences(ClipPreferencesSetter & clipPreferences)700 CropPlugin::getClipPreferences(ClipPreferencesSetter &clipPreferences)
701 {
702     if ( _reformat->getValue() ) {
703         GeneratorExtentEnum extent = (GeneratorExtentEnum)_extent->getValue();
704 
705         OfxRectI pixelFormat = {0, 0, 0, 0};
706         double par = 0.;
707         if (extent == eGeneratorExtentFormat) {
708             //specific output format
709             par = _formatPar->getValue();
710             int w, h;
711             _formatSize->getValue(w, h);
712             pixelFormat.x1 = pixelFormat.y1 = 0;
713             pixelFormat.x2 = w;
714             pixelFormat.y2 = h;
715         }
716         if (extent == eGeneratorExtentProject) {
717             OfxPointD siz = getProjectSize();
718             OfxPointD off = getProjectOffset();
719             par = getProjectPixelAspectRatio();
720             OfxRectD rod;
721             rod.x1 = off.x;
722             rod.x2 = off.x + siz.x;
723             rod.y1 = off.y;
724             rod.y2 = off.y + siz.y;
725             const OfxPointD rsOne = {1., 1.};
726             Coords::toPixelNearest(rod, rsOne, par, &pixelFormat);
727         }
728         if (par != 0.) {
729             clipPreferences.setPixelAspectRatio(*_dstClip, par);
730         }
731 #ifdef OFX_EXTENSIONS_NATRON
732         if ( !Coords::rectIsEmpty(pixelFormat) ) {
733             clipPreferences.setOutputFormat(pixelFormat);
734         }
735 #endif
736     }
737 }
738 
739 class CropInteract
740     : public RectangleInteract
741 {
742 public:
743 
CropInteract(OfxInteractHandle handle,ImageEffect * effect)744     CropInteract(OfxInteractHandle handle,
745                  ImageEffect* effect)
746         : RectangleInteract(handle, effect)
747         , _reformat(NULL)
748         , _isReformated(false)
749     {
750         _reformat = effect->fetchBooleanParam(kParamReformat);
751         addParamToSlaveTo(_reformat);
752         assert(_reformat);
753     }
754 
755 private:
756 
getBtmLeft(OfxTime time) const757     virtual OfxPointD getBtmLeft(OfxTime time) const OVERRIDE FINAL
758     {
759         OfxPointD btmLeft;
760         bool reformat;
761 
762         _reformat->getValueAtTime(time, reformat);
763         if (!reformat) {
764             btmLeft = RectangleInteract::getBtmLeft(time);
765         } else {
766             btmLeft.x = btmLeft.y = 0.;
767         }
768 
769         return btmLeft;
770     }
771 
aboutToCheckInteractivity(OfxTime time)772     virtual void aboutToCheckInteractivity(OfxTime time) OVERRIDE FINAL
773     {
774         updateReformated(time);
775     }
776 
allowTopLeftInteraction() const777     virtual bool allowTopLeftInteraction() const OVERRIDE FINAL { return !_isReformated; }
778 
allowBtmRightInteraction() const779     virtual bool allowBtmRightInteraction() const OVERRIDE FINAL { return !_isReformated; }
780 
allowBtmLeftInteraction() const781     virtual bool allowBtmLeftInteraction() const OVERRIDE FINAL { return !_isReformated; }
782 
allowBtmMidInteraction() const783     virtual bool allowBtmMidInteraction() const OVERRIDE FINAL { return !_isReformated; }
784 
allowMidLeftInteraction() const785     virtual bool allowMidLeftInteraction() const OVERRIDE FINAL { return !_isReformated; }
786 
allowCenterInteraction() const787     virtual bool allowCenterInteraction() const OVERRIDE FINAL { return !_isReformated; }
788 
updateReformated(OfxTime time)789     void updateReformated(OfxTime time)
790     {
791         _reformat->getValueAtTime(time, _isReformated);
792     }
793 
794 private:
795     BooleanParam* _reformat;
796     bool _isReformated; //< @see aboutToCheckInteractivity
797 };
798 
799 class CropOverlayDescriptor
800     : public DefaultEffectOverlayDescriptor<CropOverlayDescriptor, CropInteract>
801 {
802 };
803 
804 
805 mDeclarePluginFactory(CropPluginFactory, {ofxsThreadSuiteCheck();}, {});
806 
807 void
describe(ImageEffectDescriptor & desc)808 CropPluginFactory::describe(ImageEffectDescriptor &desc)
809 {
810     // basic labels
811     desc.setLabel(kPluginName);
812     desc.setPluginGrouping(kPluginGrouping);
813     desc.setPluginDescription(kPluginDescription);
814 
815     desc.addSupportedContext(eContextGeneral);
816     desc.addSupportedContext(eContextFilter);
817 
818     desc.addSupportedBitDepth(eBitDepthUByte);
819     desc.addSupportedBitDepth(eBitDepthUShort);
820     desc.addSupportedBitDepth(eBitDepthFloat);
821 
822 
823     desc.setSingleInstance(false);
824     desc.setHostFrameThreading(false);
825     desc.setTemporalClipAccess(false);
826     desc.setRenderTwiceAlways(true);
827     desc.setSupportsMultipleClipPARs(kSupportsMultipleClipPARs);
828     desc.setSupportsMultipleClipDepths(kSupportsMultipleClipDepths);
829     desc.setRenderThreadSafety(kRenderThreadSafety);
830 
831     desc.setSupportsTiles(kSupportsTiles);
832 
833     // in order to support multiresolution, render() must take into account the pixelaspectratio and the renderscale
834     // and scale the transform appropriately.
835     // All other functions are usually in canonical coordinates.
836     desc.setSupportsMultiResolution(kSupportsMultiResolution);
837     desc.setOverlayInteractDescriptor(new CropOverlayDescriptor);
838 #ifdef OFX_EXTENSIONS_NUKE
839     // ask the host to render all planes
840     desc.setPassThroughForNotProcessedPlanes(ePassThroughLevelRenderAllRequestedPlanes);
841 #endif
842 #ifdef OFX_EXTENSIONS_NATRON
843     desc.setChannelSelector(ePixelComponentNone);
844 #endif
845 }
846 
847 ImageEffect*
createInstance(OfxImageEffectHandle handle,ContextEnum)848 CropPluginFactory::createInstance(OfxImageEffectHandle handle,
849                                   ContextEnum /*context*/)
850 {
851     return new CropPlugin(handle);
852 }
853 
854 void
describeInContext(ImageEffectDescriptor & desc,ContextEnum context)855 CropPluginFactory::describeInContext(ImageEffectDescriptor &desc,
856                                      ContextEnum context)
857 {
858     // Source clip only in the filter context
859     // create the mandated source clip
860     // always declare the source clip first, because some hosts may consider
861     // it as the default input clip (e.g. Nuke)
862     ClipDescriptor *srcClip = desc.defineClip(kOfxImageEffectSimpleSourceClipName);
863 
864     srcClip->addSupportedComponent(ePixelComponentRGBA);
865     srcClip->addSupportedComponent(ePixelComponentRGB);
866 #ifdef OFX_EXTENSIONS_NATRON
867     srcClip->addSupportedComponent(ePixelComponentXY);
868 #endif
869     srcClip->addSupportedComponent(ePixelComponentAlpha);
870     srcClip->setTemporalClipAccess(false);
871     srcClip->setSupportsTiles(kSupportsTiles);
872     srcClip->setIsMask(false);
873 
874     // create the mandated output clip
875     ClipDescriptor *dstClip = desc.defineClip(kOfxImageEffectOutputClipName);
876     dstClip->addSupportedComponent(ePixelComponentRGBA);
877     dstClip->addSupportedComponent(ePixelComponentRGB);
878 #ifdef OFX_EXTENSIONS_NATRON
879     dstClip->addSupportedComponent(ePixelComponentXY);
880 #endif
881     dstClip->addSupportedComponent(ePixelComponentAlpha);
882     dstClip->setSupportsTiles(kSupportsTiles);
883 
884     // make some pages and to things in
885     PageParamDescriptor *page = desc.definePageParam("Controls");
886 
887     // rectangleInteractEnable
888     {
889         BooleanParamDescriptor* param = desc.defineBooleanParam(kParamRectangleInteractEnable);
890         param->setIsSecretAndDisabled(true);
891         param->setDefault(!kParamReformatDefault);
892         if (page) {
893             page->addChild(*param);
894         }
895     }
896     generatorDescribeInContext(page, desc, /*unused*/ *dstClip, eGeneratorExtentSize, /*unused*/ ePixelComponentRGBA, /*useOutputComponentsAndDepth=*/ false, context, /*reformat=*/ false);
897 
898     // softness
899     {
900         DoubleParamDescriptor* param = desc.defineDoubleParam(kParamSoftness);
901         param->setLabel(kParamSoftnessLabel);
902         param->setDefault(0);
903         param->setRange(0., 1000.);
904         param->setDisplayRange(0., 100.);
905         param->setIncrement(1.);
906         param->setHint(kParamSoftnessHint);
907         if (page) {
908             page->addChild(*param);
909         }
910     }
911 
912     // reformat
913     {
914         BooleanParamDescriptor* param = desc.defineBooleanParam(kParamReformat);
915         param->setLabel(kParamReformatLabel);
916         param->setHint( std::string(kParamReformatHint) + (getImageEffectHostDescription()->isNatron ? kParamReformatHintExtraNatron : "") );
917         param->setDefault(kParamReformatDefault);
918         param->setAnimates(false);
919         param->setLayoutHint(eLayoutHintNoNewLine, 1);
920         desc.addClipPreferencesSlaveParam(*param);
921         if (page) {
922             page->addChild(*param);
923         }
924     }
925 
926     // intersect
927     {
928         BooleanParamDescriptor* param = desc.defineBooleanParam(kParamIntersect);
929         param->setLabel(kParamIntersectLabel);
930         param->setHint(kParamIntersectHint);
931         param->setLayoutHint(eLayoutHintNoNewLine, 1);
932         param->setDefault(false);
933         param->setAnimates(true);
934         if (page) {
935             page->addChild(*param);
936         }
937     }
938 
939     // blackOutside
940     {
941         BooleanParamDescriptor* param = desc.defineBooleanParam(kParamBlackOutside);
942         param->setLabel(kParamBlackOutsideLabel);
943         param->setDefault(false);
944         param->setAnimates(true);
945         param->setHint(kParamBlackOutsideHint);
946         if (page) {
947             page->addChild(*param);
948         }
949     }
950 } // CropPluginFactory::describeInContext
951 
952 static CropPluginFactory p(kPluginIdentifier, kPluginVersionMajor, kPluginVersionMinor);
953 mRegisterPluginFactoryInstance(p)
954 
955 OFXS_NAMESPACE_ANONYMOUS_EXIT
956