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