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 ¶mName) 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 ¶mName)
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