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 Saturation plugin.
21 */
22
23 #include <cmath>
24 #include <cfloat> // DBL_MAX
25 #include <algorithm>
26
27 #include "ofxsProcessing.H"
28 #include "ofxsMaskMix.h"
29 #include "ofxsCoords.h"
30 #include "ofxsLut.h"
31 #include "ofxsMacros.h"
32 #ifdef OFX_EXTENSIONS_NATRON
33 #include "ofxNatron.h"
34 #endif
35
36 using namespace OFX;
37
38 OFXS_NAMESPACE_ANONYMOUS_ENTER
39
40 #define kPluginName "SaturationOFX"
41 #define kPluginGrouping "Color"
42 #define kPluginDescription "Modify the color saturation of an image.\n" \
43 "See also: http://opticalenquiry.com/nuke/index.php?title=Saturation"
44
45 #define kPluginIdentifier "net.sf.openfx.SaturationPlugin"
46 // History:
47 // version 1.0: initial version
48 // version 2.0: use kNatronOfxParamProcess* parameters
49 #define kPluginVersionMajor 2 // Incrementing this number means that you have broken backwards compatibility of the plug-in.
50 #define kPluginVersionMinor 0 // Increment this when you have fixed a bug or made it faster.
51
52 #define kSupportsTiles 1
53 #define kSupportsMultiResolution 1
54 #define kSupportsRenderScale 1
55 #define kSupportsMultipleClipPARs false
56 #define kSupportsMultipleClipDepths false
57 #define kRenderThreadSafety eRenderFullySafe
58
59 #ifdef OFX_EXTENSIONS_NATRON
60 #define kParamProcessR kNatronOfxParamProcessR
61 #define kParamProcessRLabel kNatronOfxParamProcessRLabel
62 #define kParamProcessRHint kNatronOfxParamProcessRHint
63 #define kParamProcessG kNatronOfxParamProcessG
64 #define kParamProcessGLabel kNatronOfxParamProcessGLabel
65 #define kParamProcessGHint kNatronOfxParamProcessGHint
66 #define kParamProcessB kNatronOfxParamProcessB
67 #define kParamProcessBLabel kNatronOfxParamProcessBLabel
68 #define kParamProcessBHint kNatronOfxParamProcessBHint
69 #define kParamProcessA kNatronOfxParamProcessA
70 #define kParamProcessALabel kNatronOfxParamProcessALabel
71 #define kParamProcessAHint kNatronOfxParamProcessAHint
72 #else
73 #define kParamProcessR "processR"
74 #define kParamProcessRLabel "R"
75 #define kParamProcessRHint "Process red component."
76 #define kParamProcessG "processG"
77 #define kParamProcessGLabel "G"
78 #define kParamProcessGHint "Process green component."
79 #define kParamProcessB "processB"
80 #define kParamProcessBLabel "B"
81 #define kParamProcessBHint "Process blue component."
82 #define kParamProcessA "processA"
83 #define kParamProcessALabel "A"
84 #define kParamProcessAHint "Process alpha component."
85 #endif
86
87 #define kParamSaturation "saturation"
88 #define kParamSaturationLabel "Saturation"
89 #define kParamSaturationHint "Color saturation factor to apply. 0 produces grayscale."
90
91 #define kParamLuminanceMath "luminanceMath"
92 #define kParamLuminanceMathLabel "Luminance Math"
93 #define kParamLuminanceMathHint "Formula used to compute luminance from RGB values."
94 #define kParamLuminanceMathOptionRec709 "Rec. 709", "Use Rec. 709 (0.2126r + 0.7152g + 0.0722b).", "rec709"
95 #define kParamLuminanceMathOptionRec2020 "Rec. 2020", "Use Rec. 2020 (0.2627r + 0.6780g + 0.0593b).", "rec2020"
96 #define kParamLuminanceMathOptionACESAP0 "ACES AP0", "Use ACES AP0 (0.3439664498r + 0.7281660966g + -0.0721325464b).", "acesap0"
97 #define kParamLuminanceMathOptionACESAP1 "ACES AP1", "Use ACES AP1 (0.2722287168r + 0.6740817658g + 0.0536895174b).", "acesap1"
98 #define kParamLuminanceMathOptionCcir601 "CCIR 601", "Use CCIR 601 (0.2989r + 0.5866g + 0.1145b).", "ccir601"
99 #define kParamLuminanceMathOptionAverage "Average", "Use average of r, g, b.", "average"
100 #define kParamLuminanceMathOptionMaximum "Max", "Use max or r, g, b.", "max"
101
102 enum LuminanceMathEnum
103 {
104 eLuminanceMathRec709,
105 eLuminanceMathRec2020,
106 eLuminanceMathACESAP0,
107 eLuminanceMathACESAP1,
108 eLuminanceMathCcir601,
109 eLuminanceMathAverage,
110 eLuminanceMathMaximum,
111 };
112
113 #define kParamClampBlack "clampBlack"
114 #define kParamClampBlackLabel "Clamp Black"
115 #define kParamClampBlackHint "All colors below 0 on output are set to 0."
116
117 #define kParamClampWhite "clampWhite"
118 #define kParamClampWhiteLabel "Clamp White"
119 #define kParamClampWhiteHint "All colors above 1 on output are set to 1."
120
121 #define kParamPremultChanged "premultChanged"
122
123
124 class SaturationProcessorBase
125 : public ImageProcessor
126 {
127 protected:
128 const Image *_srcImg;
129 const Image *_maskImg;
130 bool _premult;
131 int _premultChannel;
132 bool _doMasking;
133 double _mix;
134 bool _maskInvert;
135 bool _processR, _processG, _processB, _processA;
136
137 public:
138
SaturationProcessorBase(ImageEffect & instance)139 SaturationProcessorBase(ImageEffect &instance)
140 : ImageProcessor(instance)
141 , _srcImg(NULL)
142 , _maskImg(NULL)
143 , _premult(false)
144 , _premultChannel(3)
145 , _doMasking(false)
146 , _mix(1.)
147 , _maskInvert(false)
148 , _processR(false)
149 , _processG(false)
150 , _processB(false)
151 , _processA(false)
152 , _saturation(0.)
153 , _luminanceMath(eLuminanceMathRec709)
154 , _clampBlack(true)
155 , _clampWhite(true)
156 {
157 }
158
setSrcImg(const Image * v)159 void setSrcImg(const Image *v)
160 {
161 _srcImg = v;
162 }
163
setMaskImg(const Image * v,bool maskInvert)164 void setMaskImg(const Image *v,
165 bool maskInvert)
166 {
167 _maskImg = v; _maskInvert = maskInvert;
168 }
169
doMasking(bool v)170 void doMasking(bool v)
171 {
172 _doMasking = v;
173 }
174
setValues(double saturation,LuminanceMathEnum luminanceMath,bool clampBlack,bool clampWhite,bool premult,int premultChannel,double mix,bool processR,bool processG,bool processB,bool processA)175 void setValues(double saturation,
176 LuminanceMathEnum luminanceMath,
177 bool clampBlack,
178 bool clampWhite,
179 bool premult,
180 int premultChannel,
181 double mix,
182 bool processR,
183 bool processG,
184 bool processB,
185 bool processA)
186 {
187 _saturation = saturation;
188 _luminanceMath = luminanceMath;
189 _clampBlack = clampBlack;
190 _clampWhite = clampWhite;
191 _premult = premult;
192 _premultChannel = premultChannel;
193 _mix = mix;
194 _processR = processR;
195 _processG = processG;
196 _processB = processB;
197 _processA = processA;
198 }
199
200 template<bool processR, bool processG, bool processB, bool processA>
grade(double * r,double * g,double * b,double * a)201 void grade(double *r,
202 double *g,
203 double *b,
204 double *a)
205 {
206 double l;
207
208 switch (_luminanceMath) {
209 case eLuminanceMathRec709:
210 l = Color::rgb709_to_y(*r, *g, *b);
211 break;
212
213 case eLuminanceMathRec2020: // https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2087-0-201510-I!!PDF-E.pdf
214 l = Color::rgb2020_to_y(*r, *g, *b);
215 break;
216
217 case eLuminanceMathACESAP0: // https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#Converting_ACES_RGB_values_to_CIE_XYZ_values
218 l = Color::rgbACESAP0_to_y(*r, *g, *b);
219 break;
220
221 case eLuminanceMathACESAP1: // https://en.wikipedia.org/wiki/Academy_Color_Encoding_System#Converting_ACES_RGB_values_to_CIE_XYZ_values
222 l = Color::rgbACESAP1_to_y(*r, *g, *b);
223
224 case eLuminanceMathCcir601:
225 l = 0.2989 * *r + 0.5866 * *g + 0.1145 * *b;
226 break;
227
228 case eLuminanceMathAverage:
229 l = (*r + *g + *b) / 3;
230 break;
231
232 case eLuminanceMathMaximum:
233 l = std::max(std::max(*r, *g), *b);
234 break;
235 }
236 if (processR) {
237 *r = (1. - _saturation) * l + _saturation * *r;
238 }
239 if (processG) {
240 *g = (1. - _saturation) * l + _saturation * *g;
241 }
242 if (processB) {
243 *b = (1. - _saturation) * l + _saturation * *b;
244 }
245 if (processA) {
246 // nothing to do
247 }
248 if (_clampBlack) {
249 if (processR) {
250 *r = std::max(0., *r);
251 }
252 if (processG) {
253 *g = std::max(0., *g);
254 }
255 if (processB) {
256 *b = std::max(0., *b);
257 }
258 if (processA) {
259 *a = std::max(0., *a);
260 }
261 }
262 if (_clampWhite) {
263 if (processR) {
264 *r = std::min(1., *r);
265 }
266 if (processG) {
267 *g = std::min(1., *g);
268 }
269 if (processB) {
270 *b = std::min(1., *b);
271 }
272 if (processA) {
273 *a = std::min(1., *a);
274 }
275 }
276 } // grade
277
278 private:
279 double _saturation;
280 LuminanceMathEnum _luminanceMath;
281 bool _clampBlack;
282 bool _clampWhite;
283 };
284
285
286 template <class PIX, int nComponents, int maxValue>
287 class SaturationProcessor
288 : public SaturationProcessorBase
289 {
290 public:
SaturationProcessor(ImageEffect & instance)291 SaturationProcessor(ImageEffect &instance)
292 : SaturationProcessorBase(instance)
293 {
294 }
295
multiThreadProcessImages(OfxRectI procWindow)296 void multiThreadProcessImages(OfxRectI procWindow)
297 {
298 # ifndef __COVERITY__ // too many coverity[dead_error_line] errors
299 const bool r = _processR && (nComponents != 1);
300 const bool g = _processG && (nComponents >= 2);
301 const bool b = _processB && (nComponents >= 3);
302 const bool a = _processA && (nComponents == 1 || nComponents == 4);
303 if (r) {
304 if (g) {
305 if (b) {
306 if (a) {
307 return process<true, true, true, true >(procWindow); // RGBA
308 } else {
309 return process<true, true, true, false>(procWindow); // RGBa
310 }
311 } else {
312 if (a) {
313 return process<true, true, false, true >(procWindow); // RGbA
314 } else {
315 return process<true, true, false, false>(procWindow); // RGba
316 }
317 }
318 } else {
319 if (b) {
320 if (a) {
321 return process<true, false, true, true >(procWindow); // RgBA
322 } else {
323 return process<true, false, true, false>(procWindow); // RgBa
324 }
325 } else {
326 if (a) {
327 return process<true, false, false, true >(procWindow); // RgbA
328 } else {
329 return process<true, false, false, false>(procWindow); // Rgba
330 }
331 }
332 }
333 } else {
334 if (g) {
335 if (b) {
336 if (a) {
337 return process<false, true, true, true >(procWindow); // rGBA
338 } else {
339 return process<false, true, true, false>(procWindow); // rGBa
340 }
341 } else {
342 if (a) {
343 return process<false, true, false, true >(procWindow); // rGbA
344 } else {
345 return process<false, true, false, false>(procWindow); // rGba
346 }
347 }
348 } else {
349 if (b) {
350 if (a) {
351 return process<false, false, true, true >(procWindow); // rgBA
352 } else {
353 return process<false, false, true, false>(procWindow); // rgBa
354 }
355 } else {
356 if (a) {
357 return process<false, false, false, true >(procWindow); // rgbA
358 } else {
359 return process<false, false, false, false>(procWindow); // rgba
360 }
361 }
362 }
363 }
364 # endif // ifndef __COVERITY__
365 } // multiThreadProcessImages
366
367 private:
368
369 template<bool processR, bool processG, bool processB, bool processA>
process(OfxRectI procWindow)370 void process(OfxRectI procWindow)
371 {
372 assert( (!processR && !processG && !processB) || (nComponents == 3 || nComponents == 4) );
373 assert( !processA || (nComponents == 1 || nComponents == 4) );
374 assert(nComponents == 3 || nComponents == 4);
375 assert(_dstImg);
376 float unpPix[4];
377 float tmpPix[4];
378 for (int y = procWindow.y1; y < procWindow.y2; y++) {
379 if ( _effect.abort() ) {
380 break;
381 }
382
383 PIX *dstPix = (PIX *) _dstImg->getPixelAddress(procWindow.x1, y);
384
385 for (int x = procWindow.x1; x < procWindow.x2; x++) {
386 const PIX *srcPix = (const PIX *) (_srcImg ? _srcImg->getPixelAddress(x, y) : 0);
387 ofxsUnPremult<PIX, nComponents, maxValue>(srcPix, unpPix, _premult, _premultChannel);
388 double t_r = unpPix[0];
389 double t_g = unpPix[1];
390 double t_b = unpPix[2];
391 double t_a = unpPix[3];
392 grade<processR, processG, processB, processA>(&t_r, &t_g, &t_b, &t_a);
393 tmpPix[0] = (float)t_r;
394 tmpPix[1] = (float)t_g;
395 tmpPix[2] = (float)t_b;
396 tmpPix[3] = (float)t_a;
397 ofxsPremultMaskMixPix<PIX, nComponents, maxValue, true>(tmpPix, _premult, _premultChannel, x, y, srcPix, _doMasking, _maskImg, (float)_mix, _maskInvert, dstPix);
398 // copy back original values from unprocessed channels
399 if (nComponents == 1) {
400 if (!processA) {
401 dstPix[0] = srcPix ? srcPix[0] : PIX();
402 }
403 } else if ( (nComponents == 3) || (nComponents == 4) ) {
404 if (!processR) {
405 dstPix[0] = srcPix ? srcPix[0] : PIX();
406 }
407 if (!processG) {
408 dstPix[1] = srcPix ? srcPix[1] : PIX();
409 }
410 if (!processB) {
411 dstPix[2] = srcPix ? srcPix[2] : PIX();
412 }
413 if ( !processA && (nComponents == 4) ) {
414 dstPix[3] = srcPix ? srcPix[3] : PIX();
415 }
416 }
417 // increment the dst pixel
418 dstPix += nComponents;
419 }
420 }
421 } // process
422 };
423
424
425 ////////////////////////////////////////////////////////////////////////////////
426 /** @brief The plugin that does our work */
427 class SaturationPlugin
428 : public ImageEffect
429 {
430 public:
431 /** @brief ctor */
SaturationPlugin(OfxImageEffectHandle handle)432 SaturationPlugin(OfxImageEffectHandle handle)
433 : ImageEffect(handle)
434 , _dstClip(NULL)
435 , _srcClip(NULL)
436 , _maskClip(NULL)
437 , _processR(NULL)
438 , _processG(NULL)
439 , _processB(NULL)
440 , _processA(NULL)
441 , _saturation(NULL)
442 , _luminanceMath(NULL)
443 , _clampBlack(NULL)
444 , _clampWhite(NULL)
445 , _premult(NULL)
446 , _premultChannel(NULL)
447 , _mix(NULL)
448 , _maskApply(NULL)
449 , _maskInvert(NULL)
450 , _premultChanged(NULL)
451 {
452 _dstClip = fetchClip(kOfxImageEffectOutputClipName);
453 assert( _dstClip && (!_dstClip->isConnected() || _dstClip->getPixelComponents() == ePixelComponentRGB ||
454 _dstClip->getPixelComponents() == ePixelComponentRGBA) );
455 _srcClip = getContext() == eContextGenerator ? NULL : fetchClip(kOfxImageEffectSimpleSourceClipName);
456 assert( (!_srcClip && getContext() == eContextGenerator) ||
457 ( _srcClip && (!_srcClip->isConnected() || _srcClip->getPixelComponents() == ePixelComponentRGB ||
458 _srcClip->getPixelComponents() == ePixelComponentRGBA) ) );
459 _maskClip = fetchClip(getContext() == eContextPaint ? "Brush" : "Mask");
460 assert(!_maskClip || !_maskClip->isConnected() || _maskClip->getPixelComponents() == ePixelComponentAlpha);
461 _saturation = fetchDoubleParam(kParamSaturation);
462 _luminanceMath = fetchChoiceParam(kParamLuminanceMath);
463 _clampBlack = fetchBooleanParam(kParamClampBlack);
464 _clampWhite = fetchBooleanParam(kParamClampWhite);
465 assert(_saturation && _luminanceMath && _clampBlack && _clampWhite);
466 _premult = fetchBooleanParam(kParamPremult);
467 _premultChannel = fetchChoiceParam(kParamPremultChannel);
468 assert(_premult && _premultChannel);
469 _mix = fetchDoubleParam(kParamMix);
470 _maskApply = ( ofxsMaskIsAlwaysConnected( OFX::getImageEffectHostDescription() ) && paramExists(kParamMaskApply) ) ? fetchBooleanParam(kParamMaskApply) : 0;
471 _maskInvert = fetchBooleanParam(kParamMaskInvert);
472 assert(_mix && _maskInvert);
473 _premultChanged = fetchBooleanParam(kParamPremultChanged);
474 assert(_premultChanged);
475
476 _processR = fetchBooleanParam(kParamProcessR);
477 _processG = fetchBooleanParam(kParamProcessG);
478 _processB = fetchBooleanParam(kParamProcessB);
479 _processA = fetchBooleanParam(kParamProcessA);
480 assert(_processR && _processG && _processB && _processA);
481 }
482
483 private:
484 /* Override the render */
485 virtual void render(const RenderArguments &args) OVERRIDE FINAL;
486
487 /* set up and run a processor */
488 void setupAndProcess(SaturationProcessorBase &, const RenderArguments &args);
489
490 virtual bool isIdentity(const IsIdentityArguments &args, Clip * &identityClip, double &identityTime, int& view, std::string& plane) OVERRIDE FINAL;
491
492 /** @brief called when a clip has just been changed in some way (a rewire maybe) */
493 virtual void changedClip(const InstanceChangedArgs &args, const std::string &clipName) OVERRIDE FINAL;
494 virtual void changedParam(const InstanceChangedArgs &args, const std::string ¶mName) OVERRIDE FINAL;
495
496 private:
497 // do not need to delete these, the ImageEffect is managing them for us
498 Clip *_dstClip;
499 Clip *_srcClip;
500 Clip *_maskClip;
501 BooleanParam* _processR;
502 BooleanParam* _processG;
503 BooleanParam* _processB;
504 BooleanParam* _processA;
505 DoubleParam* _saturation;
506 ChoiceParam* _luminanceMath;
507 BooleanParam* _clampBlack;
508 BooleanParam* _clampWhite;
509 BooleanParam* _premult;
510 ChoiceParam* _premultChannel;
511 DoubleParam* _mix;
512 BooleanParam* _maskApply;
513 BooleanParam* _maskInvert;
514 BooleanParam* _premultChanged; // set to true the first time the user connects src
515 };
516
517
518 ////////////////////////////////////////////////////////////////////////////////
519 /** @brief render for the filter */
520
521 ////////////////////////////////////////////////////////////////////////////////
522 // basic plugin render function, just a skelington to instantiate templates from
523
524 /* set up and run a processor */
525 void
setupAndProcess(SaturationProcessorBase & processor,const RenderArguments & args)526 SaturationPlugin::setupAndProcess(SaturationProcessorBase &processor,
527 const RenderArguments &args)
528 {
529 const double time = args.time;
530
531 auto_ptr<Image> dst( _dstClip->fetchImage(time) );
532
533 if ( !dst.get() ) {
534 throwSuiteStatusException(kOfxStatFailed);
535 }
536 BitDepthEnum dstBitDepth = dst->getPixelDepth();
537 PixelComponentEnum dstComponents = dst->getPixelComponents();
538 if ( ( dstBitDepth != _dstClip->getPixelDepth() ) ||
539 ( dstComponents != _dstClip->getPixelComponents() ) ) {
540 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong depth or components");
541 throwSuiteStatusException(kOfxStatFailed);
542 }
543 if ( (dst->getRenderScale().x != args.renderScale.x) ||
544 ( dst->getRenderScale().y != args.renderScale.y) ||
545 ( ( dst->getField() != eFieldNone) /* for DaVinci Resolve */ && ( dst->getField() != args.fieldToRender) ) ) {
546 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
547 throwSuiteStatusException(kOfxStatFailed);
548 }
549 auto_ptr<const Image> src( ( _srcClip && _srcClip->isConnected() ) ?
550 _srcClip->fetchImage(time) : 0 );
551 if ( src.get() ) {
552 if ( (src->getRenderScale().x != args.renderScale.x) ||
553 ( src->getRenderScale().y != args.renderScale.y) ||
554 ( ( src->getField() != eFieldNone) /* for DaVinci Resolve */ && ( src->getField() != args.fieldToRender) ) ) {
555 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
556 throwSuiteStatusException(kOfxStatFailed);
557 }
558 BitDepthEnum srcBitDepth = src->getPixelDepth();
559 PixelComponentEnum srcComponents = src->getPixelComponents();
560 if ( (srcBitDepth != dstBitDepth) || (srcComponents != dstComponents) ) {
561 throwSuiteStatusException(kOfxStatErrImageFormat);
562 }
563 }
564 bool doMasking = ( ( !_maskApply || _maskApply->getValueAtTime(time) ) && _maskClip && _maskClip->isConnected() );
565 auto_ptr<const Image> mask(doMasking ? _maskClip->fetchImage(time) : 0);
566 if (doMasking) {
567 if ( mask.get() ) {
568 if ( (mask->getRenderScale().x != args.renderScale.x) ||
569 ( mask->getRenderScale().y != args.renderScale.y) ||
570 ( ( mask->getField() != eFieldNone) /* for DaVinci Resolve */ && ( mask->getField() != args.fieldToRender) ) ) {
571 setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
572 throwSuiteStatusException(kOfxStatFailed);
573 }
574 }
575 bool maskInvert = _maskInvert->getValueAtTime(time);
576 processor.doMasking(true);
577 processor.setMaskImg(mask.get(), maskInvert);
578 }
579
580 processor.setDstImg( dst.get() );
581 processor.setSrcImg( src.get() );
582 processor.setRenderWindow(args.renderWindow);
583
584 double saturation = _saturation->getValueAtTime(time);
585 LuminanceMathEnum luminanceMath = (LuminanceMathEnum)_luminanceMath->getValueAtTime(time);
586 bool clampBlack = _clampBlack->getValueAtTime(time);
587 bool clampWhite = _clampWhite->getValueAtTime(time);
588 bool premult = _premult->getValueAtTime(time);
589 int premultChannel = _premultChannel->getValueAtTime(time);
590 double mix = _mix->getValueAtTime(time);
591 bool processR = _processR->getValueAtTime(time);
592 bool processG = _processG->getValueAtTime(time);
593 bool processB = _processB->getValueAtTime(time);
594 bool processA = _processA->getValueAtTime(time);
595
596 processor.setValues(saturation, luminanceMath,
597 clampBlack, clampWhite, premult, premultChannel, mix,
598 processR, processG, processB, processA);
599 processor.process();
600 } // SaturationPlugin::setupAndProcess
601
602 // the overridden render function
603 void
render(const RenderArguments & args)604 SaturationPlugin::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 == ePixelComponentRGB || dstComponents == ePixelComponentRGBA);
613 if (dstComponents == ePixelComponentRGBA) {
614 switch (dstBitDepth) {
615 case eBitDepthUByte: {
616 SaturationProcessor<unsigned char, 4, 255> fred(*this);
617 setupAndProcess(fred, args);
618 break;
619 }
620 case eBitDepthUShort: {
621 SaturationProcessor<unsigned short, 4, 65535> fred(*this);
622 setupAndProcess(fred, args);
623 break;
624 }
625 case eBitDepthFloat: {
626 SaturationProcessor<float, 4, 1> fred(*this);
627 setupAndProcess(fred, args);
628 break;
629 }
630 default:
631 throwSuiteStatusException(kOfxStatErrUnsupported);
632 }
633 } else {
634 assert(dstComponents == ePixelComponentRGB);
635 switch (dstBitDepth) {
636 case eBitDepthUByte: {
637 SaturationProcessor<unsigned char, 3, 255> fred(*this);
638 setupAndProcess(fred, args);
639 break;
640 }
641 case eBitDepthUShort: {
642 SaturationProcessor<unsigned short, 3, 65535> fred(*this);
643 setupAndProcess(fred, args);
644 break;
645 }
646 case eBitDepthFloat: {
647 SaturationProcessor<float, 3, 1> fred(*this);
648 setupAndProcess(fred, args);
649 break;
650 }
651 default:
652 throwSuiteStatusException(kOfxStatErrUnsupported);
653 }
654 }
655 } // SaturationPlugin::render
656
657 bool
isIdentity(const IsIdentityArguments & args,Clip * & identityClip,double &,int &,std::string &)658 SaturationPlugin::isIdentity(const IsIdentityArguments &args,
659 Clip * &identityClip,
660 double & /*identityTime*/
661 , int& /*view*/, std::string& /*plane*/)
662 {
663 const double time = args.time;
664 double mix = _mix->getValueAtTime(time);
665
666 if (mix == 0.) {
667 identityClip = _srcClip;
668
669 return true;
670 }
671
672 {
673 bool processR = _processR->getValueAtTime(time);
674 bool processG = _processG->getValueAtTime(time);
675 bool processB = _processB->getValueAtTime(time);
676 bool processA = _processA->getValueAtTime(time);
677 if (!processR && !processG && !processB && !processA) {
678 identityClip = _srcClip;
679
680 return true;
681 }
682 }
683
684 bool clampBlack = _clampBlack->getValueAtTime(time);
685 bool clampWhite = _clampWhite->getValueAtTime(time);
686 if (clampBlack || clampWhite) {
687 return false;
688 }
689
690 double saturation = _saturation->getValueAtTime(time);
691 if (saturation == 1) {
692 identityClip = _srcClip;
693
694 return true;
695 }
696
697 bool doMasking = ( ( !_maskApply || _maskApply->getValueAtTime(time) ) && _maskClip && _maskClip->isConnected() );
698 if (doMasking) {
699 bool maskInvert = _maskInvert->getValueAtTime(time);
700 if (!maskInvert) {
701 OfxRectI maskRoD;
702 Coords::toPixelEnclosing(_maskClip->getRegionOfDefinition(time), args.renderScale, _maskClip->getPixelAspectRatio(), &maskRoD);
703 // effect is identity if the renderWindow doesn't intersect the mask RoD
704 if ( !Coords::rectIntersection<OfxRectI>(args.renderWindow, maskRoD, 0) ) {
705 identityClip = _srcClip;
706
707 return true;
708 }
709 }
710 }
711
712 return false;
713 } // SaturationPlugin::isIdentity
714
715 void
changedClip(const InstanceChangedArgs & args,const std::string & clipName)716 SaturationPlugin::changedClip(const InstanceChangedArgs &args,
717 const std::string &clipName)
718 {
719 if ( (clipName == kOfxImageEffectSimpleSourceClipName) &&
720 _srcClip && _srcClip->isConnected() &&
721 !_premultChanged->getValue() &&
722 ( args.reason == eChangeUserEdit) ) {
723 switch ( _srcClip->getPreMultiplication() ) {
724 case eImageOpaque:
725 _premult->setValue(false);
726 break;
727 case eImagePreMultiplied:
728 _premult->setValue(true);
729 break;
730 case eImageUnPreMultiplied:
731 _premult->setValue(false);
732 break;
733 }
734 }
735 }
736
737 void
changedParam(const InstanceChangedArgs & args,const std::string & paramName)738 SaturationPlugin::changedParam(const InstanceChangedArgs &args,
739 const std::string ¶mName)
740 {
741 if ( (paramName == kParamPremult) && (args.reason == eChangeUserEdit) ) {
742 _premultChanged->setValue(true);
743 }
744 }
745
746 mDeclarePluginFactory(SaturationPluginFactory, {ofxsThreadSuiteCheck();}, {});
747 void
describe(ImageEffectDescriptor & desc)748 SaturationPluginFactory::describe(ImageEffectDescriptor &desc)
749 {
750 // basic labels
751 desc.setLabel(kPluginName);
752 desc.setPluginGrouping(kPluginGrouping);
753 desc.setPluginDescription(kPluginDescription);
754
755 desc.addSupportedContext(eContextFilter);
756 desc.addSupportedContext(eContextGeneral);
757 desc.addSupportedContext(eContextPaint);
758 desc.addSupportedBitDepth(eBitDepthUByte);
759 desc.addSupportedBitDepth(eBitDepthUShort);
760 desc.addSupportedBitDepth(eBitDepthFloat);
761
762 // set a few flags
763 desc.setSingleInstance(false);
764 desc.setHostFrameThreading(false);
765 desc.setSupportsMultiResolution(kSupportsMultiResolution);
766 desc.setSupportsTiles(kSupportsTiles);
767 desc.setTemporalClipAccess(false);
768 desc.setRenderTwiceAlways(false);
769 desc.setSupportsMultipleClipPARs(kSupportsMultipleClipPARs);
770 desc.setSupportsMultipleClipDepths(kSupportsMultipleClipDepths);
771 desc.setRenderThreadSafety(kRenderThreadSafety);
772 #ifdef OFX_EXTENSIONS_NATRON
773 desc.setChannelSelector(ePixelComponentNone); // we have our own channel selector
774 #endif
775 }
776
777 void
describeInContext(ImageEffectDescriptor & desc,ContextEnum context)778 SaturationPluginFactory::describeInContext(ImageEffectDescriptor &desc,
779 ContextEnum context)
780 {
781 // Source clip only in the filter context
782 // create the mandated source clip
783 ClipDescriptor *srcClip = desc.defineClip(kOfxImageEffectSimpleSourceClipName);
784
785 srcClip->addSupportedComponent(ePixelComponentRGBA);
786 srcClip->addSupportedComponent(ePixelComponentRGB);
787 srcClip->setTemporalClipAccess(false);
788 srcClip->setSupportsTiles(kSupportsTiles);
789 srcClip->setIsMask(false);
790
791 // create the mandated output clip
792 ClipDescriptor *dstClip = desc.defineClip(kOfxImageEffectOutputClipName);
793 dstClip->addSupportedComponent(ePixelComponentRGBA);
794 dstClip->addSupportedComponent(ePixelComponentRGB);
795 dstClip->setSupportsTiles(kSupportsTiles);
796
797 ClipDescriptor *maskClip = (context == eContextPaint) ? desc.defineClip("Brush") : desc.defineClip("Mask");
798 maskClip->addSupportedComponent(ePixelComponentAlpha);
799 maskClip->setTemporalClipAccess(false);
800 if (context != eContextPaint) {
801 maskClip->setOptional(true);
802 }
803 maskClip->setSupportsTiles(kSupportsTiles);
804 maskClip->setIsMask(true);
805
806 // make some pages and to things in
807 PageParamDescriptor *page = desc.definePageParam("Controls");
808
809 {
810 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamProcessR);
811 param->setLabel(kParamProcessRLabel);
812 param->setHint(kParamProcessRHint);
813 param->setDefault(true);
814 param->setLayoutHint(eLayoutHintNoNewLine, 1);
815 if (page) {
816 page->addChild(*param);
817 }
818 }
819 {
820 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamProcessG);
821 param->setLabel(kParamProcessGLabel);
822 param->setHint(kParamProcessGHint);
823 param->setDefault(true);
824 param->setLayoutHint(eLayoutHintNoNewLine, 1);
825 if (page) {
826 page->addChild(*param);
827 }
828 }
829 {
830 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamProcessB);
831 param->setLabel(kParamProcessBLabel);
832 param->setHint(kParamProcessBHint);
833 param->setDefault(true);
834 param->setLayoutHint(eLayoutHintNoNewLine, 1);
835 if (page) {
836 page->addChild(*param);
837 }
838 }
839 {
840 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamProcessA);
841 param->setLabel(kParamProcessALabel);
842 param->setHint(kParamProcessAHint);
843 param->setDefault(false);
844 if (page) {
845 page->addChild(*param);
846 }
847 }
848
849 {
850 DoubleParamDescriptor* param = desc.defineDoubleParam(kParamSaturation);
851 param->setLabel(kParamSaturationLabel);
852 param->setHint(kParamSaturationHint);
853 param->setRange(0., DBL_MAX); // Resolve requires range and display range or values are clamped to (-1,1)
854 param->setDisplayRange(0., 4.);
855 param->setDefault(1.);
856 if (page) {
857 page->addChild(*param);
858 }
859 }
860 {
861 ChoiceParamDescriptor* param = desc.defineChoiceParam(kParamLuminanceMath);
862 param->setLabel(kParamLuminanceMathLabel);
863 param->setHint(kParamLuminanceMathHint);
864 assert(param->getNOptions() == eLuminanceMathRec709);
865 param->appendOption(kParamLuminanceMathOptionRec709);
866 assert(param->getNOptions() == eLuminanceMathRec2020);
867 param->appendOption(kParamLuminanceMathOptionRec2020);
868 assert(param->getNOptions() == eLuminanceMathACESAP0);
869 param->appendOption(kParamLuminanceMathOptionACESAP0);
870 assert(param->getNOptions() == eLuminanceMathACESAP1);
871 param->appendOption(kParamLuminanceMathOptionACESAP1);
872 assert(param->getNOptions() == eLuminanceMathCcir601);
873 param->appendOption(kParamLuminanceMathOptionCcir601);
874 assert(param->getNOptions() == eLuminanceMathAverage);
875 param->appendOption(kParamLuminanceMathOptionAverage);
876 assert(param->getNOptions() == eLuminanceMathMaximum);
877 param->appendOption(kParamLuminanceMathOptionMaximum);
878 if (page) {
879 page->addChild(*param);
880 }
881 }
882
883 {
884 BooleanParamDescriptor *param = desc.defineBooleanParam(kParamClampBlack);
885 param->setLabel(kParamClampBlackLabel);
886 param->setHint(kParamClampBlackHint);
887 param->setDefault(true);
888 param->setAnimates(true);
889 param->setLayoutHint(eLayoutHintNoNewLine, 0);
890 if (page) {
891 page->addChild(*param);
892 }
893 }
894 {
895 BooleanParamDescriptor *param = desc.defineBooleanParam(kParamClampWhite);
896 param->setLabel(kParamClampWhiteLabel);
897 param->setHint(kParamClampWhiteHint);
898 param->setDefault(false);
899 param->setAnimates(true);
900 if (page) {
901 page->addChild(*param);
902 }
903 }
904
905 ofxsPremultDescribeParams(desc, page);
906 ofxsMaskMixDescribeParams(desc, page);
907
908 {
909 BooleanParamDescriptor* param = desc.defineBooleanParam(kParamPremultChanged);
910 param->setDefault(false);
911 param->setIsSecretAndDisabled(true);
912 param->setAnimates(false);
913 param->setEvaluateOnChange(false);
914 if (page) {
915 page->addChild(*param);
916 }
917 }
918 } // SaturationPluginFactory::describeInContext
919
920 ImageEffect*
createInstance(OfxImageEffectHandle handle,ContextEnum)921 SaturationPluginFactory::createInstance(OfxImageEffectHandle handle,
922 ContextEnum /*context*/)
923 {
924 return new SaturationPlugin(handle);
925 }
926
927 static SaturationPluginFactory p(kPluginIdentifier, kPluginVersionMajor, kPluginVersionMinor);
928 mRegisterPluginFactoryInstance(p)
929
930 OFXS_NAMESPACE_ANONYMOUS_EXIT
931