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 SpriteSheet 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 "ofxsMacros.h"
30 #include "ofxsThreadSuite.h"
31 
32 using namespace OFX;
33 
34 OFXS_NAMESPACE_ANONYMOUS_ENTER
35 
36 #define kPluginName "SpriteSheet"
37 #define kPluginGrouping "Transform"
38 #define kPluginDescription "Read individual frames from a sprite sheet. A sprite sheet is a series of images (usually animation frames) combined into a larger image (or images). For example, an animation consisting of eight 100x100 images could be combined into a single 400x200 sprite sheet (4 frames across by 2 high). The sprite with index 0 is at the top-left of the source image, and sprites are ordered left-to-right and top-to-bottom. The output is an animated sprite that repeats the sprites given in the sprite range. The ContactSheet effect can be used to make a spritesheet from a series of images or a video."
39 #define kPluginIdentifier "net.sf.openfx.SpriteSheet"
40 #define kPluginVersionMajor 1 // Incrementing this number means that you have broken backwards compatibility of the plug-in.
41 #define kPluginVersionMinor 0 // Increment this when you have fixed a bug or made it faster.
42 
43 #define kSupportsTiles 1
44 #define kSupportsMultiResolution 1
45 #define kSupportsRenderScale 1
46 #define kSupportsMultipleClipPARs false
47 #define kSupportsMultipleClipDepths false
48 #define kRenderThreadSafety eRenderFullySafe
49 
50 #define kParamSpriteSize "spriteSize"
51 #define kParamSpriteSizeLabel "Sprite Size", "Size in pixels of an individual sprite."
52 
53 #define kParamSpriteRange "spriteRange"
54 #define kParamSpriteRangeLabel "Sprite Range", "Index of the first and last sprite in the animation. The sprite index starts at zero."
55 
56 #define kParamFrameOffset "frameOffset"
57 #define kParamFrameOffsetLabel "Frame Offset", "Output frame number for the first sprite."
58 
59 #ifdef OFX_EXTENSIONS_NATRON
60 #define OFX_COMPONENTS_OK(c) ((c)== ePixelComponentAlpha || (c) == ePixelComponentXY || (c) == ePixelComponentRGB || (c) == ePixelComponentRGBA)
61 #else
62 #define OFX_COMPONENTS_OK(c) ((c)== ePixelComponentAlpha || (c) == ePixelComponentRGB || (c) == ePixelComponentRGBA)
63 #endif
64 
65 
66 class SpriteSheetProcessorBase
67     : public ImageProcessor
68 {
69 protected:
70     const Image *_srcImg;
71     OfxRectI _cropRectPixel;
72 
73 public:
SpriteSheetProcessorBase(ImageEffect & instance)74     SpriteSheetProcessorBase(ImageEffect &instance)
75         : ImageProcessor(instance)
76         , _srcImg(NULL)
77     {
78         _cropRectPixel.x1 = _cropRectPixel.y1 = _cropRectPixel.x2 = _cropRectPixel.y2 = 0;
79     }
80 
81     /** @brief set the src image */
setSrcImg(const Image * v)82     void setSrcImg(const Image *v)
83     {
84         _srcImg = v;
85     }
86 
setValues(const OfxRectI & cropRectPixel)87     void setValues(const OfxRectI& cropRectPixel)
88     {
89         _cropRectPixel = cropRectPixel;
90     }
91 };
92 
93 
94 template <class PIX, int nComponents, int maxValue>
95 class SpriteSheetProcessor
96     : public SpriteSheetProcessorBase
97 {
98 public:
SpriteSheetProcessor(ImageEffect & instance)99     SpriteSheetProcessor(ImageEffect &instance)
100         : SpriteSheetProcessorBase(instance)
101     {
102     }
103 
104 private:
multiThreadProcessImages(OfxRectI procWindow)105     void multiThreadProcessImages(OfxRectI procWindow)
106     {
107         for (int y = procWindow.y1; y < procWindow.y2; ++y) {
108             if ( _effect.abort() ) {
109                 break;
110             }
111 
112             PIX *dstPix = (PIX *) _dstImg->getPixelAddress(procWindow.x1, y);
113 
114             for (int x = procWindow.x1; x < procWindow.x2; ++x, dstPix += nComponents) {
115                 const PIX *srcPix = _srcImg ? (const PIX*)_srcImg->getPixelAddress(x + _cropRectPixel.x1, y + _cropRectPixel.y1) : 0;
116                 if (srcPix) {
117                     // inside of the rectangle
118                     for (int k = 0; k < nComponents; ++k) {
119                         dstPix[k] = srcPix[k];
120                     }
121                 } else {
122                     for (int k = 0; k < nComponents; ++k) {
123                         dstPix[k] = 0;
124                     }
125                 }
126             }
127         }
128     } // multiThreadProcessImages
129 };
130 
131 // the modulo oprator (always returns a positive number)
132 static inline
133 int
mod(int a,int b)134 mod(int a, int b)
135 {
136     return a >= 0 ? a % b : ( b - std::abs ( a%b ) ) % b;
137 }
138 
139 ////////////////////////////////////////////////////////////////////////////////
140 /** @brief The plugin that does our work */
141 class SpriteSheetPlugin
142     : public ImageEffect
143 {
144 public:
145     /** @brief ctor */
SpriteSheetPlugin(OfxImageEffectHandle handle)146     SpriteSheetPlugin(OfxImageEffectHandle handle)
147         : ImageEffect(handle)
148         , _dstClip(NULL)
149         , _srcClip(NULL)
150         , _spriteSize(NULL)
151         , _spriteRange(NULL)
152         , _frameOffset(NULL)
153     {
154         _dstClip = fetchClip(kOfxImageEffectOutputClipName);
155         assert( _dstClip && (!_dstClip->isConnected() || _dstClip->getPixelComponents() == ePixelComponentAlpha ||
156                              _dstClip->getPixelComponents() == ePixelComponentRGB ||
157                              _dstClip->getPixelComponents() == ePixelComponentRGBA) );
158         _srcClip = getContext() == eContextGenerator ? NULL : fetchClip(kOfxImageEffectSimpleSourceClipName);
159         assert( (!_srcClip && getContext() == eContextGenerator) ||
160                 ( _srcClip && (!_srcClip->isConnected() || _srcClip->getPixelComponents() ==  ePixelComponentAlpha ||
161                                _srcClip->getPixelComponents() == ePixelComponentRGB ||
162                                _srcClip->getPixelComponents() == ePixelComponentRGBA) ) );
163 
164         _spriteSize = fetchInt2DParam(kParamSpriteSize);
165         _spriteRange = fetchInt2DParam(kParamSpriteRange);
166         _frameOffset = fetchIntParam(kParamFrameOffset);
167     }
168 
169 private:
170     // override the roi call
171     virtual void getRegionsOfInterest(const RegionsOfInterestArguments &args, RegionOfInterestSetter &rois) OVERRIDE FINAL;
172     virtual bool getRegionOfDefinition(const RegionOfDefinitionArguments &args, OfxRectD &rod) OVERRIDE FINAL;
173 
174     /* Override the render */
175     virtual void render(const RenderArguments &args) OVERRIDE FINAL;
176 
177     template <int nComponents>
178     void renderInternal(const RenderArguments &args, BitDepthEnum dstBitDepth);
179 
180     /* set up and run a processor */
181     void setupAndProcess(SpriteSheetProcessorBase &, const RenderArguments &args);
182 
183     //virtual void changedParam(const InstanceChangedArgs &args, const std::string &paramName) OVERRIDE FINAL;
184     virtual void getClipPreferences(ClipPreferencesSetter &clipPreferences) OVERRIDE FINAL;
185 
getCropRectangle(OfxTime time,const OfxPointD & renderScale,const OfxRectI & rodPixel,const OfxPointI & spriteSize,const OfxPointI & spriteRange,int frameOffset,OfxRectI * cropRectPixel) const186     void getCropRectangle(OfxTime time,
187                           const OfxPointD& renderScale,
188                           const OfxRectI& rodPixel, // RoD in pixel at renderscale = 1
189                           const OfxPointI& spriteSize,
190                           const OfxPointI& spriteRange,
191                           int frameOffset,
192                           OfxRectI *cropRectPixel) const
193     {
194         // number of sprites in the range
195         int n = std::abs(spriteRange.y - spriteRange.x) + 1;
196         // sprite index
197         int i = mod((int)std::floor(time) - frameOffset, n);
198         if (spriteRange.x <= spriteRange.y) {
199             i = spriteRange.x + i;
200         } else {
201             i = spriteRange.x - i;
202         }
203         // number of sprites per line
204         int cols = (rodPixel.x2 - rodPixel.x1) / spriteSize.x;
205         if (cols <= 0) {
206             cols = 1;
207         }
208         int r = i / cols;
209         int c = i % cols;
210         cropRectPixel->x1 = (int)(renderScale.x * (rodPixel.x1 + c * spriteSize.x)); // left to right
211         cropRectPixel->y1 = (int)(renderScale.y * (rodPixel.y2 - (r + 1) * spriteSize.y)); // top to bottom
212         cropRectPixel->x2 = (int)(renderScale.x * (rodPixel.x1 + (c + 1) * spriteSize.x));
213         cropRectPixel->y2 = (int)(renderScale.y * (rodPixel.y2 - r * spriteSize.y));
214     } // SpriteSheetPlugin::getCropRectangle
215 
getSrcClip() const216     Clip* getSrcClip() const
217     {
218         return _srcClip;
219     }
220 
221 private:
222     // do not need to delete these, the ImageEffect is managing them for us
223     Clip *_dstClip;
224     Clip *_srcClip;
225     Int2DParam* _spriteSize;
226     Int2DParam* _spriteRange;
227     IntParam* _frameOffset;
228 };
229 
230 
231 ////////////////////////////////////////////////////////////////////////////////
232 /** @brief render for the filter */
233 
234 ////////////////////////////////////////////////////////////////////////////////
235 // basic plugin render function, just a skelington to instantiate templates from
236 
237 /* set up and run a processor */
238 void
setupAndProcess(SpriteSheetProcessorBase & processor,const RenderArguments & args)239 SpriteSheetPlugin::setupAndProcess(SpriteSheetProcessorBase &processor,
240                             const RenderArguments &args)
241 {
242     const double time = args.time;
243 
244     auto_ptr<Image> dst( _dstClip->fetchImage(args.time) );
245 
246     if ( !dst.get() ) {
247         throwSuiteStatusException(kOfxStatFailed);
248     }
249     BitDepthEnum dstBitDepth    = dst->getPixelDepth();
250     PixelComponentEnum dstComponents  = dst->getPixelComponents();
251     if ( ( dstBitDepth != _dstClip->getPixelDepth() ) ||
252          ( dstComponents != _dstClip->getPixelComponents() ) ) {
253         setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong depth or components");
254         throwSuiteStatusException(kOfxStatFailed);
255     }
256     if ( (dst->getRenderScale().x != args.renderScale.x) ||
257          ( dst->getRenderScale().y != args.renderScale.y) ||
258          ( ( dst->getField() != eFieldNone) /* for DaVinci Resolve */ && ( dst->getField() != args.fieldToRender) ) ) {
259         setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
260         throwSuiteStatusException(kOfxStatFailed);
261     }
262     auto_ptr<const Image> src( ( _srcClip && _srcClip->isConnected() ) ?
263                                     _srcClip->fetchImage(args.time) : 0 );
264     if ( !src.get() || !( _srcClip && _srcClip->isConnected() ) ) {
265         // nothing to do
266         return;
267     } else {
268         if ( (src->getRenderScale().x != args.renderScale.x) ||
269              ( src->getRenderScale().y != args.renderScale.y) ||
270              ( ( src->getField() != eFieldNone) /* for DaVinci Resolve */ && ( src->getField() != args.fieldToRender) ) ) {
271             setPersistentMessage(Message::eMessageError, "", "OFX Host gave image with wrong scale or field properties");
272             throwSuiteStatusException(kOfxStatFailed);
273         }
274         BitDepthEnum dstBitDepth       = dst->getPixelDepth();
275         PixelComponentEnum dstComponents  = dst->getPixelComponents();
276         BitDepthEnum srcBitDepth      = src->getPixelDepth();
277         PixelComponentEnum srcComponents = src->getPixelComponents();
278         if ( (srcBitDepth != dstBitDepth) || (srcComponents != dstComponents) ) {
279             throwSuiteStatusException(kOfxStatFailed);
280         }
281     }
282 
283     // set the images
284     processor.setDstImg( dst.get() );
285     processor.setSrcImg( src.get() );
286 
287     // set the render window
288     processor.setRenderWindow(args.renderWindow);
289 
290 
291     // get the input format (Natron only) or the input RoD (others)
292     OfxRectI srcRoDPixel;
293     _srcClip->getFormat(srcRoDPixel);
294     if ( OFX::Coords::rectIsEmpty(srcRoDPixel) ) {
295         // no format is available, use the RoD instead
296         OfxRectD srcRoD = _srcClip->getRegionOfDefinition(time);
297         double par = _srcClip->getPixelAspectRatio();
298         const OfxPointD rs1 = {1., 1.};
299         Coords::toPixelNearest(srcRoD, rs1, par, &srcRoDPixel);
300     }
301     OfxPointI spriteSize;
302     _spriteSize->getValueAtTime(time, spriteSize.x, spriteSize.y);
303     OfxPointI spriteRange;
304     _spriteRange->getValueAtTime(time, spriteRange.x, spriteRange.y);
305     int frameOffset = _frameOffset->getValueAtTime(time);
306 
307     OfxRectI cropRectPixel;
308     getCropRectangle(time, args.renderScale, srcRoDPixel, spriteSize, spriteRange, frameOffset, &cropRectPixel);
309 
310     processor.setValues(cropRectPixel);
311 
312     // Call the base class process member, this will call the derived templated process code
313     processor.process();
314 } // SpriteSheetPlugin::setupAndProcess
315 
316 // override the roi call
317 // Required if the plugin requires a region from the inputs which is different from the rendered region of the output.
318 // (this is the case here)
319 void
getRegionsOfInterest(const RegionsOfInterestArguments & args,RegionOfInterestSetter & rois)320 SpriteSheetPlugin::getRegionsOfInterest(const RegionsOfInterestArguments &args,
321                                  RegionOfInterestSetter &rois)
322 {
323     const double time = args.time;
324 
325     // get the input format (Natron only) or the input RoD (others)
326     OfxRectI srcRoDPixel;
327     _srcClip->getFormat(srcRoDPixel);
328     double par = _srcClip->getPixelAspectRatio();
329     if ( OFX::Coords::rectIsEmpty(srcRoDPixel) ) {
330         // no format is available, use the RoD instead
331         OfxRectD srcRoD = _srcClip->getRegionOfDefinition(time);
332         const OfxPointD rs1 = {1., 1.};
333         Coords::toPixelNearest(srcRoD, rs1, par, &srcRoDPixel);
334     }
335     OfxPointI spriteSize;
336     _spriteSize->getValueAtTime(time, spriteSize.x, spriteSize.y);
337     OfxPointI spriteRange;
338     _spriteRange->getValueAtTime(time, spriteRange.x, spriteRange.y);
339     int frameOffset = _frameOffset->getValueAtTime(time);
340 
341     OfxRectI cropRectPixel;
342     getCropRectangle(time, args.renderScale, srcRoDPixel, spriteSize, spriteRange, frameOffset, &cropRectPixel);
343 
344     OfxRectD cropRect;
345     Coords::toCanonical(cropRectPixel, args.renderScale, par, &cropRect);
346 
347     OfxRectD roi = args.regionOfInterest;
348     roi.x1 += cropRect.x1;
349     roi.y1 += cropRect.y1;
350     roi.x2 += cropRect.x1;
351     roi.y2 += cropRect.y1;
352 
353     rois.setRegionOfInterest(*_srcClip, roi);
354 }
355 
356 bool
getRegionOfDefinition(const RegionOfDefinitionArguments & args,OfxRectD & rod)357 SpriteSheetPlugin::getRegionOfDefinition(const RegionOfDefinitionArguments &args,
358                                   OfxRectD &rod)
359 {
360     const double time = args.time;
361 
362     double par = _srcClip->getPixelAspectRatio();
363     const OfxPointD rs1 = {1., 1.};
364     OfxRectI sprite = {0, 0, 0, 0};
365     _spriteSize->getValueAtTime(time, sprite.x2, sprite.y2);
366 
367     Coords::toCanonical(sprite, rs1, par, &rod);
368 
369     return true;
370 }
371 
372 // the internal render function
373 template <int nComponents>
374 void
renderInternal(const RenderArguments & args,BitDepthEnum dstBitDepth)375 SpriteSheetPlugin::renderInternal(const RenderArguments &args,
376                            BitDepthEnum dstBitDepth)
377 {
378     switch (dstBitDepth) {
379     case eBitDepthUByte: {
380         SpriteSheetProcessor<unsigned char, nComponents, 255> fred(*this);
381         setupAndProcess(fred, args);
382         break;
383     }
384     case eBitDepthUShort: {
385         SpriteSheetProcessor<unsigned short, nComponents, 65535> fred(*this);
386         setupAndProcess(fred, args);
387         break;
388     }
389     case eBitDepthFloat: {
390         SpriteSheetProcessor<float, nComponents, 1> fred(*this);
391         setupAndProcess(fred, args);
392         break;
393     }
394     default:
395         throwSuiteStatusException(kOfxStatErrUnsupported);
396     }
397 }
398 
399 // the overridden render function
400 void
render(const RenderArguments & args)401 SpriteSheetPlugin::render(const RenderArguments &args)
402 {
403     // instantiate the render code based on the pixel depth of the dst clip
404     BitDepthEnum dstBitDepth    = _dstClip->getPixelDepth();
405     PixelComponentEnum dstComponents  = _dstClip->getPixelComponents();
406 
407     assert( kSupportsMultipleClipPARs   || !_srcClip || _srcClip->getPixelAspectRatio() == _dstClip->getPixelAspectRatio() );
408     assert( kSupportsMultipleClipDepths || !_srcClip || _srcClip->getPixelDepth()       == _dstClip->getPixelDepth() );
409     assert(OFX_COMPONENTS_OK(dstComponents));
410     if (dstComponents == ePixelComponentRGBA) {
411         renderInternal<4>(args, dstBitDepth);
412     } else if (dstComponents == ePixelComponentRGB) {
413         renderInternal<3>(args, dstBitDepth);
414 #ifdef OFX_EXTENSIONS_NATRON
415     } else if (dstComponents == ePixelComponentXY) {
416         renderInternal<2>(args, dstBitDepth);
417 #endif
418     } else {
419         assert(dstComponents == ePixelComponentAlpha);
420         renderInternal<1>(args, dstBitDepth);
421     }
422 }
423 
424 void
getClipPreferences(ClipPreferencesSetter & clipPreferences)425 SpriteSheetPlugin::getClipPreferences(ClipPreferencesSetter &clipPreferences)
426 {
427 #ifdef OFX_EXTENSIONS_NATRON
428     OfxRectI pixelFormat = {0, 0, 0, 0};
429     _spriteSize->getValue(pixelFormat.x2, pixelFormat.y2);
430     if ( !Coords::rectIsEmpty(pixelFormat) ) {
431         clipPreferences.setOutputFormat(pixelFormat);
432     }
433 #endif
434     clipPreferences.setOutputFrameVarying(true); // because output depends on the frame number
435 }
436 
437 
438 mDeclarePluginFactory(SpriteSheetPluginFactory, {ofxsThreadSuiteCheck();}, {});
439 
440 void
describe(ImageEffectDescriptor & desc)441 SpriteSheetPluginFactory::describe(ImageEffectDescriptor &desc)
442 {
443     // basic labels
444     desc.setLabel(kPluginName);
445     desc.setPluginGrouping(kPluginGrouping);
446     desc.setPluginDescription(kPluginDescription);
447 
448     desc.addSupportedContext(eContextGeneral);
449     desc.addSupportedContext(eContextFilter);
450 
451     desc.addSupportedBitDepth(eBitDepthUByte);
452     desc.addSupportedBitDepth(eBitDepthUShort);
453     desc.addSupportedBitDepth(eBitDepthFloat);
454 
455 
456     desc.setSingleInstance(false);
457     desc.setHostFrameThreading(false);
458     desc.setTemporalClipAccess(false);
459     desc.setRenderTwiceAlways(true);
460     desc.setSupportsMultipleClipPARs(kSupportsMultipleClipPARs);
461     desc.setSupportsMultipleClipDepths(kSupportsMultipleClipDepths);
462     desc.setRenderThreadSafety(kRenderThreadSafety);
463 
464     desc.setSupportsTiles(kSupportsTiles);
465 
466     // in order to support multiresolution, render() must take into account the pixelaspectratio and the renderscale
467     // and scale the transform appropriately.
468     // All other functions are usually in canonical coordinates.
469     desc.setSupportsMultiResolution(kSupportsMultiResolution);
470 #ifdef OFX_EXTENSIONS_NUKE
471     // ask the host to render all planes
472     desc.setPassThroughForNotProcessedPlanes(ePassThroughLevelRenderAllRequestedPlanes);
473 #endif
474 #ifdef OFX_EXTENSIONS_NATRON
475     desc.setChannelSelector(ePixelComponentNone);
476 #endif
477 }
478 
479 ImageEffect*
createInstance(OfxImageEffectHandle handle,ContextEnum)480 SpriteSheetPluginFactory::createInstance(OfxImageEffectHandle handle,
481                                   ContextEnum /*context*/)
482 {
483     return new SpriteSheetPlugin(handle);
484 }
485 
486 void
describeInContext(ImageEffectDescriptor & desc,ContextEnum)487 SpriteSheetPluginFactory::describeInContext(ImageEffectDescriptor &desc,
488                                      ContextEnum /*context*/)
489 {
490     // Source clip only in the filter context
491     // create the mandated source clip
492     // always declare the source clip first, because some hosts may consider
493     // it as the default input clip (e.g. Nuke)
494     ClipDescriptor *srcClip = desc.defineClip(kOfxImageEffectSimpleSourceClipName);
495 
496     srcClip->addSupportedComponent(ePixelComponentRGBA);
497     srcClip->addSupportedComponent(ePixelComponentRGB);
498 #ifdef OFX_EXTENSIONS_NATRON
499     srcClip->addSupportedComponent(ePixelComponentXY);
500 #endif
501     srcClip->addSupportedComponent(ePixelComponentAlpha);
502     srcClip->setTemporalClipAccess(false);
503     srcClip->setSupportsTiles(kSupportsTiles);
504     srcClip->setIsMask(false);
505 
506     // create the mandated output clip
507     ClipDescriptor *dstClip = desc.defineClip(kOfxImageEffectOutputClipName);
508     dstClip->addSupportedComponent(ePixelComponentRGBA);
509     dstClip->addSupportedComponent(ePixelComponentRGB);
510 #ifdef OFX_EXTENSIONS_NATRON
511     dstClip->addSupportedComponent(ePixelComponentXY);
512 #endif
513     dstClip->addSupportedComponent(ePixelComponentAlpha);
514     dstClip->setSupportsTiles(kSupportsTiles);
515 
516     // make some pages and to things in
517     PageParamDescriptor *page = desc.definePageParam("Controls");
518 
519     {
520         Int2DParamDescriptor* param = desc.defineInt2DParam(kParamSpriteSize);
521         param->setLabelAndHint(kParamSpriteSizeLabel);
522         param->setRange(1, 1, INT_MAX, INT_MAX);
523         param->setDisplayRange(1, 1, 512, 512);
524         param->setDefault(64, 64);
525         param->setAnimates(false);
526 #ifdef OFX_EXTENSIONS_NATRON
527         desc.addClipPreferencesSlaveParam(*param);
528 #endif
529         if (page) {
530             page->addChild(*param);
531         }
532     }
533     {
534         Int2DParamDescriptor* param = desc.defineInt2DParam(kParamSpriteRange);
535         param->setLabelAndHint(kParamSpriteRangeLabel);
536         param->setRange(0, 0, INT_MAX, INT_MAX);
537         param->setDefault(0, 0);
538         param->setDimensionLabels("first", "last");
539         if (page) {
540             page->addChild(*param);
541         }
542     }
543     {
544         IntParamDescriptor* param = desc.defineIntParam(kParamFrameOffset);
545         param->setLabelAndHint(kParamFrameOffsetLabel);
546         param->setRange(INT_MIN, INT_MAX);
547         param->setDefault(1);
548         if (page) {
549             page->addChild(*param);
550         }
551     }
552 } // SpriteSheetPluginFactory::describeInContext
553 
554 static SpriteSheetPluginFactory p(kPluginIdentifier, kPluginVersionMajor, kPluginVersionMinor);
555 mRegisterPluginFactoryInstance(p)
556 
557 OFXS_NAMESPACE_ANONYMOUS_EXIT
558