1 /* ***** BEGIN LICENSE BLOCK ***** 2 * This file is part of openfx-io <https://github.com/MrKepzie/openfx-io>, 3 * Copyright (C) 2013-2018 INRIA 4 * 5 * openfx-io 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-io 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-io. If not, see <http://www.gnu.org/licenses/gpl-2.0.html> 17 * ***** END LICENSE BLOCK ***** */ 18 19 /* 20 * OFX GenericWriter plugin. 21 * A base class for all OpenFX-based encoders. 22 */ 23 24 #ifndef Io_GenericWriter_h 25 #define Io_GenericWriter_h 26 27 #include <memory> 28 #include <ofxsImageEffect.h> 29 #include <ofxsMultiPlane.h> 30 #include "IOUtility.h" 31 #include "ofxsMacros.h" 32 #include "ofxsPixelProcessor.h" // for getImageData 33 #include "ofxsCopier.h" // for copyPixels 34 35 namespace OFX { 36 class PixelProcessorFilterBase; 37 38 namespace IO { 39 #ifdef OFX_IO_USING_OCIO 40 class GenericOCIO; 41 #endif 42 43 enum LayerViewsPartsEnum 44 { 45 eLayerViewsSinglePart = 0, 46 eLayerViewsSplitViews, 47 eLayerViewsSplitViewsLayers 48 }; 49 50 #define kGenericWriterViewDefault -2 // Indicates that we want to render what the host request via the render action (the default) 51 #define kGenericWriterViewAll -1 // the write will write all views when rendering view 0 52 53 /** 54 * @brief A generic writer plugin, derive this to create a new writer for a specific file format. 55 * This class propose to handle the common stuff among writers: 56 * - common params 57 * - a way to inform the host about the colour-space of the data. 58 **/ 59 class GenericWriterPlugin 60 : public OFX::MultiPlane::MultiPlaneEffect 61 { 62 public: 63 64 GenericWriterPlugin(OfxImageEffectHandle handle, 65 const std::vector<std::string>& extensions, 66 bool supportsRGBA, bool supportsRGB, bool supportsXY, bool supportsAlpha); 67 68 virtual ~GenericWriterPlugin(); 69 70 /** 71 * @brief Don't override this function, the GenericWriterPlugin class already does the rendering. The "encoding" of the frame 72 * must be done by the pure virtual function encode(...) instead. 73 * The render function also copies the image from the input clip to the output clip (only if the effect is connected downstream) 74 * in order to be able to branch this effect in the middle of an effect tree. 75 **/ 76 virtual void render(const OFX::RenderArguments &args) OVERRIDE FINAL; 77 78 /* override is identity */ 79 virtual bool isIdentity(const OFX::IsIdentityArguments &args, OFX::Clip * &identityClip, double &identityTime, int& view, std::string& plane) OVERRIDE; 80 81 /** @brief client begin sequence render function */ 82 virtual void beginSequenceRender(const OFX::BeginSequenceRenderArguments &args) OVERRIDE; 83 84 /** @brief client end sequence render function */ 85 virtual void endSequenceRender(const OFX::EndSequenceRenderArguments &args) OVERRIDE; 86 87 /** 88 * @brief Don't override this. It returns the projects region of definition. 89 **/ 90 virtual bool getRegionOfDefinition(const OFX::RegionOfDefinitionArguments &args, OfxRectD &rod) OVERRIDE FINAL; 91 92 /** 93 * @brief Don't override this. It returns the source region of definition. 94 **/ 95 virtual void getRegionsOfInterest(const OFX::RegionsOfInterestArguments &args, OFX::RegionOfInterestSetter &rois) OVERRIDE FINAL; 96 97 /** 98 * @brief Don't override this. It returns the frame range to render. 99 **/ 100 virtual bool getTimeDomain(OfxRangeD &range) OVERRIDE FINAL; 101 102 /** 103 * @brief You can override this to take actions in response to a param change. 104 * Make sure you call the base-class version of this function at the end: i.e: 105 * 106 * void MyReader::changedParam(const OFX::InstanceChangedArgs &args, const std::string ¶mName) { 107 * if (.....) { 108 * 109 * } else if(.....) { 110 * 111 * } else { 112 * GenericReaderPlugin::changedParam(args,paramName); 113 * } 114 * } 115 **/ 116 virtual void changedParam(const OFX::InstanceChangedArgs &args, const std::string ¶mName) OVERRIDE; 117 118 /** 119 * @brief Overriden to handle premultiplication parameter given the input clip. 120 * Make sure you call the base class implementation if you override it. 121 **/ 122 virtual void changedClip(const OFX::InstanceChangedArgs &args, const std::string &clipName) OVERRIDE; 123 124 /** 125 * @brief Overriden to set the clips premultiplication according to the user and plug-ins wishes. 126 * It also set the output components from the output components parameter 127 **/ 128 virtual void getClipPreferences(OFX::ClipPreferencesSetter &clipPreferences) OVERRIDE; 129 130 131 /** 132 * @brief Overriden to request the views needed to render. 133 **/ 134 virtual void getFrameViewsNeeded(const OFX::FrameViewsNeededArguments& args, OFX::FrameViewsNeededSetter& frameViews) OVERRIDE FINAL; 135 136 /** 137 * @brief Overriden to clear any OCIO cache. 138 * This function calls clearAnyCache() if you have any cache to clear. 139 **/ 140 virtual void purgeCaches(void) OVERRIDE FINAL; 141 142 /** 143 * @brief Restore any state from the parameters set 144 * Called from createInstance() and changedParam() (via outputFileChanged()), must restore the 145 * state of the Reader, such as Choice param options, data members and non-persistent param values. 146 * We don't do this in the ctor of the plug-in since we can't call virtuals yet. 147 * Any derived implementation must call GenericWriterPlugin::restoreStateFromParams() first 148 **/ 149 virtual void restoreStateFromParams(); 150 151 protected: 152 153 void setOutputComponentsParam(OFX::PixelComponentEnum components); 154 155 156 /** 157 * @brief Override this function to actually encode the image in the file pointed to by filename. 158 * If the file is a video-stream then you should encode the frame at the time given in parameters. 159 * You must write the decoded image into dstImg. This function should convert the pixels from srcImg 160 * into the color-space and bitdepths of the newly created images's file. 161 * You can inform the host of the bitdepth you support in input in the describe() function. 162 * You can always skip the color-space conversion, but for all linear hosts it would produce either 163 * false colors or sub-par performances in the case the end-user has to prepend a color-space conversion 164 * effect her/himself. 165 * 166 * @param filename The output file to write to 167 * @param time The frame number 168 * @param viewName The name of the view to render 169 * @param pixelData Pointer to the start of the input image 170 * @param bounds The bounds of the pixelData buffer 171 * @param pixelAspectRatio The PAR of the source image 172 * @param pixelDataNComps The number of components per pixel in pixelData 173 * @param dstNCompsStartIndex The start index where the first component of dstNComps is to be read (in the range of pixelDataNComps) 174 * @param dstNComps The desired number of components in the written file 175 * @param rowBytes The number of bytes in a row of pixelData. 176 * The following assert should hold true: 177 * assert(((bounds.x2 - bounds.x1) * pixelDataNComps * sizeof(float)) == rowBytes); 178 * 179 * @pre The filename has been validated against the supported file extensions. 180 * You don't need to check this yourself. 181 * The source image has been correctly color-converted 182 **/ 183 virtual void encode(const std::string& filename, 184 const OfxTime time, 185 const std::string& viewName, 186 const float *pixelData, 187 const OfxRectI& bounds, 188 const float pixelAspectRatio, 189 const int pixelDataNComps, 190 const int dstNCompsStartIndex, 191 const int dstNComps, 192 const int rowBytes); beginEncode(const std::string &,const OfxRectI &,float,const OFX::BeginSequenceRenderArguments &)193 virtual void beginEncode(const std::string& /*filename*/, 194 const OfxRectI& /*rodPixel*/, 195 float /*pixelAspectRatio*/, 196 const OFX::BeginSequenceRenderArguments & /*args*/) {} 197 endEncode(const OFX::EndSequenceRenderArguments &)198 virtual void endEncode(const OFX::EndSequenceRenderArguments & /*args*/) {} 199 200 friend class EncodePlanesLocalData_RAII; 201 ///Used to allocate/free userdata passed to beginEncodePlanes,endEncodePlanes and encodePlane allocateEncodePlanesUserData()202 virtual void* allocateEncodePlanesUserData() { return (void*)0; } 203 destroyEncodePlanesUserData(void *)204 virtual void destroyEncodePlanesUserData(void* /*data*/) {} 205 206 /** 207 * @brief When writing multiple planes, should allocate data that are shared amongst all planes 208 **/ 209 virtual void beginEncodeParts(void* user_data, 210 const std::string& filename, 211 OfxTime time, 212 float pixelAspectRatio, 213 LayerViewsPartsEnum partsSplitting, 214 const std::map<int, std::string>& viewsToRender, 215 const std::list<std::string>& planes, 216 const bool packingRequired, 217 const std::vector<int>& packingMapping, 218 const OfxRectI& bounds); endEncodeParts(void *)219 virtual void endEncodeParts(void* /*user_data*/) {} 220 221 virtual void encodePart(void* user_data, const std::string& filename, const float *pixelData, int pixelDataNComps, int planeIndex, int rowBytes); 222 223 /** 224 * @brief Should return the view index needed to render. 225 * Possible return values: 226 * -2 or kGenericWriterViewDefault: Indicates that we want to render what the host request via the render action (the default) 227 * -1 or kGenericWriterViewAll: Indicates that we want to render all views in a single file. In this case, rendering view 0 requires all views. 228 * >= 0: Indicates the view index to render 229 **/ getViewToRender()230 virtual int getViewToRender() const { return -2; } 231 getPartsSplittingPreference()232 virtual LayerViewsPartsEnum getPartsSplittingPreference() const { return eLayerViewsSinglePart; } 233 234 /** 235 * @brief Overload to return false if the given file extension is a video file extension or 236 * true if this is an image file extension. 237 **/ 238 virtual bool isImageFile(const std::string& fileExtension) const = 0; setOutputFrameRate(double)239 virtual void setOutputFrameRate(double /*fps*/) {} 240 241 /** 242 * @brief Must return whether your plug-in expects an input stream to be premultiplied or unpremultiplied to encode 243 * properly into the file. 244 **/ 245 virtual OFX::PreMultiplicationEnum getExpectedInputPremultiplication() const = 0; 246 247 /** 248 * @brief To implement if you added supportsDisplayWindow = true to GenericWriterDescribe(). 249 * Basically only EXR file format can handle this. 250 **/ displayWindowSupportedByFormat(const std::string &)251 virtual bool displayWindowSupportedByFormat(const std::string& /*filename*/) const { return false; } 252 253 254 OFX::Clip* _inputClip; //< Mantated input clip 255 OFX::Clip *_outputClip; //< Mandated output clip 256 OFX::StringParam *_fileParam; //< The output file 257 OFX::BooleanParam *_overwrite; //< Do we overwrite existing files? 258 OFX::ChoiceParam *_frameRange; //<The frame range type 259 OFX::IntParam* _firstFrame; //< the first frame if the frame range type is "Manual" 260 OFX::IntParam* _lastFrame; //< the last frame if the frame range type is "Manual" 261 OFX::ChoiceParam* _outputFormatType; //< the type of output format 262 OFX::ChoiceParam* _outputFormat; //< the output format to render 263 OFX::Int2DParam* _outputFormatSize; 264 OFX::DoubleParam* _outputFormatPar; 265 OFX::ChoiceParam* _premult; 266 OFX::BooleanParam* _clipToRoD; 267 268 OFX::StringParam* _sublabel; 269 OFX::BooleanParam* _processChannels[4]; 270 OFX::ChoiceParam* _outputComponents; 271 OFX::BooleanParam* _guessedParams; //!< was guessParamsFromFilename already successfully called once on this instance 272 273 #ifdef OFX_IO_USING_OCIO 274 OFX::BooleanParam* _outputSpaceSet; 275 auto_ptr<GenericOCIO> _ocio; 276 #endif 277 278 const std::vector<std::string>& _extensions; 279 bool _supportsRGBA; 280 bool _supportsRGB; 281 bool _supportsXY; 282 bool _supportsAlpha; 283 284 std::vector<OFX::PixelComponentEnum> _outputComponentsTable; 285 286 private: 287 288 289 class InputImagesHolder 290 { 291 std::list<const OFX::Image*> _imgs; 292 std::list<OFX::ImageMemory*> _mems; 293 294 public: 295 296 InputImagesHolder(); 297 void addImage(const OFX::Image* img); 298 void addMemory(OFX::ImageMemory* mem); 299 ~InputImagesHolder(); 300 }; 301 302 /* 303 * @brief Fetch the given plane for the given view at the given time and convert into suited color-space 304 * using OCIO if needed. 305 * 306 * If view == renderRequestedView the output image will be fetched on the output clip 307 * and written to aswell. 308 * 309 * Post-condition: 310 * - srcImgsHolder had the srcImg appended to it so it gets correctly released when it is 311 * destroyed. 312 * - tmpMemPtr is never NULL and points to either srcImg buffer or tmpMem buffer 313 * - If a color-space conversion occured, tmpMem/tmpMemPtr is non-null and tmpMem was added to srcImgsHolder 314 * so it gets correctly released upon destruction. 315 * 316 * This function MAY throw exceptions aborting the action, that is why we use the InputImagesHolder RAII style class 317 * that will properly release resources. 318 */ 319 void fetchPlaneConvertAndCopy(const std::string& plane, 320 bool failIfNoSrcImg, 321 int view, 322 int renderRequestedView, 323 double time, 324 const OfxRectI& renderWindow, 325 const OfxPointD& renderScale, 326 OFX::FieldEnum fieldToRender, 327 OFX::PreMultiplicationEnum pluginExpectedPremult, 328 OFX::PreMultiplicationEnum userPremult, 329 const bool isOCIOIdentity, 330 const bool doAnyPacking, 331 const bool packingContiguous, 332 const std::vector<int>& packingMapping, 333 InputImagesHolder* srcImgsHolder, 334 OfxRectI* bounds, 335 OFX::ImageMemory** tmpMem, 336 const OFX::Image** inputImage, 337 float** tmpMemPtr, 338 int* rowBytes, 339 OFX::PixelComponentEnum* mappedComponents, 340 int* mappedComponentsCount); 341 342 /** 343 * @brief Checks if the extension is supported. 344 **/ 345 bool checkExtension(const std::string& filename); 346 347 /** 348 * @brief Override if you want to do something when the output image/video file changed. 349 * You shouldn't do any strong processing as this is called on the main thread and 350 * the getRegionOfDefinition() and decode() should open the file in a separate thread. 351 **/ 352 virtual void onOutputFileChanged(const std::string& newFile, bool setColorSpace) = 0; 353 354 /** 355 * @brief Override to clear any cache you may have. 356 **/ clearAnyCache()357 virtual void clearAnyCache() {} 358 359 void getOutputRoD(OfxTime time, int view, OfxRectD* rod, double* par); 360 361 protected: 362 363 void getSelectedOutputFormat(OfxRectI* format, double* par); 364 365 void refreshRGBAParamsFromOutputComponents(); 366 367 private: 368 copyPixelData(const OfxRectI & renderWindow,const OFX::Image * srcImg,OFX::Image * dstImg)369 void copyPixelData(const OfxRectI &renderWindow, 370 const OFX::Image* srcImg, 371 OFX::Image* dstImg) 372 { 373 const void* srcPixelData; 374 OfxRectI srcBounds; 375 376 OFX::PixelComponentEnum srcPixelComponents; 377 OFX::BitDepthEnum srcBitDepth; 378 int srcRowBytes; 379 getImageData(srcImg, &srcPixelData, &srcBounds, &srcPixelComponents, &srcBitDepth, &srcRowBytes); 380 int srcPixelComponentCount = srcImg->getPixelComponentCount(); 381 void* dstPixelData; 382 OfxRectI dstBounds; 383 OFX::PixelComponentEnum dstPixelComponents; 384 OFX::BitDepthEnum dstBitDepth; 385 int dstRowBytes; 386 getImageData(dstImg, &dstPixelData, &dstBounds, &dstPixelComponents, &dstBitDepth, &dstRowBytes); 387 int dstPixelComponentCount = dstImg->getPixelComponentCount(); 388 copyPixels(*this, 389 renderWindow, 390 srcPixelData, srcBounds, srcPixelComponents, srcPixelComponentCount, srcBitDepth, srcRowBytes, 391 dstPixelData, dstBounds, dstPixelComponents, dstPixelComponentCount, dstBitDepth, dstRowBytes); 392 } 393 copyPixelData(const OfxRectI & renderWindow,const void * srcPixelData,const OfxRectI & srcBounds,OFX::PixelComponentEnum srcPixelComponents,int srcPixelComponentCount,OFX::BitDepthEnum srcBitDepth,int srcRowBytes,OFX::Image * dstImg)394 void copyPixelData(const OfxRectI &renderWindow, 395 const void *srcPixelData, 396 const OfxRectI& srcBounds, 397 OFX::PixelComponentEnum srcPixelComponents, 398 int srcPixelComponentCount, 399 OFX::BitDepthEnum srcBitDepth, 400 int srcRowBytes, 401 OFX::Image* dstImg) 402 { 403 void* dstPixelData; 404 OfxRectI dstBounds; 405 406 OFX::PixelComponentEnum dstPixelComponents; 407 OFX::BitDepthEnum dstBitDepth; 408 int dstRowBytes; 409 getImageData(dstImg, &dstPixelData, &dstBounds, &dstPixelComponents, &dstBitDepth, &dstRowBytes); 410 int dstPixelComponentCount = dstImg->getPixelComponentCount(); 411 copyPixels(*this, 412 renderWindow, 413 srcPixelData, srcBounds, srcPixelComponents, srcPixelComponentCount, srcBitDepth, srcRowBytes, 414 dstPixelData, dstBounds, dstPixelComponents, dstPixelComponentCount, dstBitDepth, dstRowBytes); 415 } 416 copyPixelData(const OfxRectI & renderWindow,const OFX::Image * srcImg,void * dstPixelData,const OfxRectI & dstBounds,OFX::PixelComponentEnum dstPixelComponents,int dstPixelComponentCount,OFX::BitDepthEnum dstBitDepth,int dstRowBytes)417 void copyPixelData(const OfxRectI &renderWindow, 418 const OFX::Image* srcImg, 419 void *dstPixelData, 420 const OfxRectI& dstBounds, 421 OFX::PixelComponentEnum dstPixelComponents, 422 int dstPixelComponentCount, 423 OFX::BitDepthEnum dstBitDepth, 424 int dstRowBytes) 425 { 426 const void* srcPixelData; 427 OfxRectI srcBounds; 428 429 OFX::PixelComponentEnum srcPixelComponents; 430 OFX::BitDepthEnum srcBitDepth; 431 int srcRowBytes; 432 getImageData(srcImg, &srcPixelData, &srcBounds, &srcPixelComponents, &srcBitDepth, &srcRowBytes); 433 int srcPixelComponentCount = srcImg->getPixelComponentCount(); 434 copyPixels(*this, 435 renderWindow, 436 srcPixelData, srcBounds, srcPixelComponents, srcPixelComponentCount, srcBitDepth, srcRowBytes, 437 dstPixelData, dstBounds, dstPixelComponents, dstPixelComponentCount, dstBitDepth, dstRowBytes); 438 } 439 440 void packPixelBuffer(const OfxRectI& renderWindow, 441 const void *srcPixelData, 442 const OfxRectI& bounds, 443 OFX::BitDepthEnum bitDepth, 444 int srcRowBytes, 445 OFX::PixelComponentEnum srcPixelComponents, 446 const std::vector<int>& channelsMapping, //maps dst channels to input channels 447 int dstRowBytes, 448 void* dstPixelData); 449 450 void interleavePixelBuffers(const OfxRectI& renderWindow, 451 const void *srcPixelData, 452 const OfxRectI& bounds, 453 const OFX::PixelComponentEnum srcPixelComponents, 454 const int srcPixelComponentCount, 455 const int srcNCompsStartIndex, 456 const int desiredSrcNComps, 457 const OFX::BitDepthEnum bitDepth, 458 const int srcRowBytes, 459 const OfxRectI& dstBounds, 460 const OFX::PixelComponentEnum dstPixelComponents, 461 const int dstPixelComponentStartIndex, 462 const int dstPixelComponentCount, 463 const int dstRowBytes, 464 void* dstPixelData); 465 466 void unPremultPixelData(const OfxRectI &renderWindow, 467 const void *srcPixelData, 468 const OfxRectI& srcBounds, 469 OFX::PixelComponentEnum srcPixelComponents, 470 int srcPixelComponentCount, 471 OFX::BitDepthEnum srcPixelDepth, 472 int srcRowBytes, 473 void *dstPixelData, 474 const OfxRectI& dstBounds, 475 OFX::PixelComponentEnum dstPixelComponents, 476 int dstPixelComponentCount, 477 OFX::BitDepthEnum dstBitDepth, 478 int dstRowBytes); 479 480 void premultPixelData(const OfxRectI &renderWindow, 481 const void *srcPixelData, 482 const OfxRectI& srcBounds, 483 OFX::PixelComponentEnum srcPixelComponents, 484 int srcPixelComponentCount, 485 OFX::BitDepthEnum srcPixelDepth, 486 int srcRowBytes, 487 void *dstPixelData, 488 const OfxRectI& dstBounds, 489 OFX::PixelComponentEnum dstPixelComponents, 490 int dstPixelComponentCount, 491 OFX::BitDepthEnum dstBitDepth, 492 int dstRowBytes); 493 494 void getPackingOptions(bool *allCheckboxHidden, std::vector<int>* packingMapping) const; 495 496 void outputFileChanged(OFX::InstanceChangeReason reason, bool restoreExistingWriter, bool throwErrors); 497 }; 498 499 class EncodePlanesLocalData_RAII 500 { 501 GenericWriterPlugin* _w; 502 void* data; 503 504 public: 505 EncodePlanesLocalData_RAII(GenericWriterPlugin * w)506 EncodePlanesLocalData_RAII(GenericWriterPlugin* w) 507 : _w(w) 508 , data(NULL) 509 { 510 data = w->allocateEncodePlanesUserData(); 511 } 512 ~EncodePlanesLocalData_RAII()513 ~EncodePlanesLocalData_RAII() 514 { 515 _w->destroyEncodePlanesUserData(data); 516 } 517 getData()518 void* getData() const { return data; } 519 }; 520 521 void GenericWriterDescribe(OFX::ImageEffectDescriptor &desc, 522 OFX::RenderSafetyEnum safety, 523 const std::vector<std::string>& extensions, 524 int evaluation, 525 bool isMultiPlanar, 526 bool isMultiView); 527 528 OFX::PageParamDescriptor* GenericWriterDescribeInContextBegin(OFX::ImageEffectDescriptor &desc, 529 OFX::ContextEnum context, 530 bool supportsRGBA, 531 bool supportsRGB, 532 bool supportsXY, 533 bool supportsAlpha, 534 const char* inputSpaceNameDefault, 535 const char* outputSpaceNameDefault, 536 bool supportsDisplayWindow); 537 538 void GenericWriterDescribeInContextEnd(OFX::ImageEffectDescriptor &desc, 539 OFX::ContextEnum context, 540 OFX::PageParamDescriptor* defaultPage); 541 542 // the load() member has to be provided, and it should fill the _extensions list of valid file extensions 543 #define mDeclareWriterPluginFactory(CLASS, UNLOADFUNCDEF, ISVIDEOSTREAM) \ 544 class CLASS \ 545 : public OFX::PluginFactoryHelper<CLASS> \ 546 { \ 547 public: \ 548 CLASS(const std::string & id, unsigned int verMaj, unsigned int verMin) \ 549 : OFX::PluginFactoryHelper<CLASS>(id, verMaj, verMin) {} \ 550 virtual void load(); \ 551 virtual void unload() UNLOADFUNCDEF; \ 552 virtual OFX::ImageEffect* createInstance(OfxImageEffectHandle handle, OFX::ContextEnum context); \ 553 bool isVideoStreamPlugin() const { return ISVIDEOSTREAM; } \ 554 virtual void describe(OFX::ImageEffectDescriptor & desc); \ 555 virtual void describeInContext(OFX::ImageEffectDescriptor & desc, OFX::ContextEnum context); \ 556 private: \ 557 std::vector<std::string> _extensions; \ 558 }; 559 } // namespace IO 560 } // namespace OFX 561 562 #endif // ifndef Io_GenericWriter_h 563