/*************************************************************************** * Copyright (c) 2005 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include "PreCompiled.h" #ifndef _PreComp_ # include # include # include # include # include # include # include # include # include # include # include # include #endif //gcc # include # include # include #include #include #include #include #include "SoFCOffscreenRenderer.h" #include "BitmapFactory.h" using namespace Gui; using namespace std; void writeJPEGComment(const std::string&, QByteArray&); // --------------------------------------------------------------- SoFCOffscreenRenderer* SoFCOffscreenRenderer::inst = 0; SoFCOffscreenRenderer& SoFCOffscreenRenderer::instance() { if (inst==0) inst = new SoFCOffscreenRenderer(SbViewportRegion()); return *inst; } SoFCOffscreenRenderer::SoFCOffscreenRenderer (const SbViewportRegion &viewportregion) : SoOffscreenRenderer(viewportregion) { } SoFCOffscreenRenderer::SoFCOffscreenRenderer (SoGLRenderAction *action) : SoOffscreenRenderer(action) { } SoFCOffscreenRenderer::~SoFCOffscreenRenderer() { } void SoFCOffscreenRenderer::writeToImage (QImage& img) const { const unsigned char * bytes = getBuffer(); SbVec2s size = getViewportRegion().getViewportSizePixels(); int numcomponents = (int) this->getComponents(); SoSFImage image; image.setValue(size, numcomponents, bytes, SoSFImage::NO_COPY); BitmapFactory().convert(image, img); } void SoFCOffscreenRenderer::writeToImageFile(const char* filename, const char* comment, const SbMatrix& mat, const QImage& image) { if (image.isNull()) { std::stringstream str; str << "Cannot save null image."; throw Base::ValueError(str.str()); } Base::FileInfo file(filename); if (file.hasExtension("JPG") || file.hasExtension("JPEG")) { // writing comment in case of jpeg (Qt ignores setText() in case of jpeg) std::string com; if (strcmp(comment,"")==0) com = "Screenshot created by FreeCAD"; else if (strcmp(comment,"$MIBA")==0) com = createMIBA(mat); else com = comment; // write into memory QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "JPG"); writeJPEGComment(com, ba); QFile file(QString::fromUtf8(filename)); if (file.open(QFile::WriteOnly)) { file.write(ba); file.close(); } else { std::stringstream str; str << "Cannot open file '" << filename << "' for writing."; throw Base::Exception(str.str()); } } else { // check for all QImage formats bool supported = false; QByteArray format; QList qtformats = QImageWriter::supportedImageFormats(); for (QList::Iterator it = qtformats.begin(); it != qtformats.end(); ++it) { if (file.hasExtension((*it).data())) { format = *it; supported = true; break; } } // Supported by Qt if (supported) { QImage img = image; // set keywords for PNG format if (file.hasExtension("PNG")) { img.setText(QLatin1String("Title"), QString::fromUtf8(filename)); img.setText(QLatin1String("Author"), QLatin1String("FreeCAD (http://www.freecadweb.org)")); if (strcmp(comment,"")==0) img.setText(QLatin1String("Description"), QLatin1String("Screenshot created by FreeCAD")); else if (strcmp(comment,"$MIBA")==0) img.setText(QLatin1String("Description"), QLatin1String(createMIBA(mat).c_str())); else img.setText(QLatin1String("Description"), QString::fromUtf8(comment)); img.setText(QLatin1String("Creation Time"), QDateTime::currentDateTime().toString()); img.setText(QLatin1String("Software"), QString::fromUtf8(App::GetApplication().getExecutableName())); } QFile f(QString::fromUtf8(filename)); if (f.open(QFile::WriteOnly)) { if (img.save(&f, format.data())) { f.close(); } else { f.close(); std::stringstream str; str << "Cannot save image to file '" << filename << "'."; throw Base::ValueError(str.str()); } } else { std::stringstream str; str << "Cannot open file '" << filename << "' for writing."; Base::FileException e; e.setMessage(str.str()); throw e; } } // // Use internal buffer instead of QImage // else if (isWriteSupported(file.extension().c_str())) { // Any format which is supported by Coin only if (!writeToFile(filename, file.extension().c_str())) throw Base::FileException("Error writing image file", filename); } else if (file.hasExtension("EPS") || file.hasExtension("PS")) { // Any format which is supported by Coin only #ifdef FC_OS_WIN32 FILE* fd = _wfopen(file.toStdWString().c_str(), L"w"); #else FILE* fd = fopen(filename, "w"); #endif bool ok = writeToPostScript(fd); fclose(fd); if (!ok) throw Base::FileException("Error writing image file", filename); } else if (file.hasExtension("RGB") || file.hasExtension("SGI")) { // Any format which is supported by Coin only #ifdef FC_OS_WIN32 FILE* fd = _wfopen(file.toStdWString().c_str(), L"w"); #else FILE* fd = fopen(filename, "w"); #endif bool ok = writeToRGB(fd); fclose(fd); if (!ok) throw Base::FileException("Error writing image file", filename); } } } QStringList SoFCOffscreenRenderer::getWriteImageFiletypeInfo() { QStringList formats; // get all supported formats by Coin3D int num = getNumWriteFiletypes(); for (int i=0; i < num; i++) { #if (COIN_MAJOR_VERSION < 2) // Coin3D <= 1.x SbList extlist; #elif (COIN_MAJOR_VERSION < 3) // Coin3D <= 2.x # if (COIN_MINOR_VERSION < 3) // Coin3D <= 2.2.x SbList extlist; # else // Coin3D >= 2.3.x SbPList extlist; # endif #else // Coin3D >= 3.x SbPList extlist; #endif SbString fullname, description; getWriteFiletypeInfo(i, extlist, fullname, description); for (int j=0; j < extlist.getLength(); j++) { QString ext = QLatin1String((const char*) extlist[j]); if (formats.indexOf(ext.toUpper()) == -1) formats << ext.toUpper(); } } // add now all further QImage formats QList qtformats = QImageWriter::supportedImageFormats(); for (QList::Iterator it = qtformats.begin(); it != qtformats.end(); ++it) { // not supported? then append if (!isWriteSupported((*it).data()) && formats.indexOf(QLatin1String(*it)) == -1) formats << QLatin1String(*it); } // now add PostScript and SGI RGB if (formats.indexOf(QLatin1String("EPS")) == -1) formats << QLatin1String("EPS"); else if (formats.indexOf(QLatin1String("SGI")) == -1) formats << QLatin1String("SGI"); formats.sort(); return formats; } std::string SoFCOffscreenRenderer::createMIBA(const SbMatrix& mat) const { std::stringstream com; const std::map& cfg = App::Application::Config(); std::map::const_iterator it; it = cfg.find("BuildVersionMajor"); std::string major = (it != cfg.end() ? it->second : ""); it = cfg.find("BuildVersionMinor"); std::string minor = (it != cfg.end() ? it->second : ""); com << setw(7) << setfill(' ') << fixed; com << " \n" ; com << " \n" ; com << " \n"; com << " \n" ; com << " \n" ; com << " \n" ; com << " Unknown\n" ; com << " " << QDateTime::currentDateTime().toString().toLatin1().constData() << "\n" ; com << " " << App::GetApplication().getExecutableName() << " " << major << "." << minor << "\n" ; com << " Unknown\n"; com << " 1.0\n"; com << " \n" ; com << "\n" ; return com.str(); } void writeJPEGComment(const std::string& comment, QByteArray& ba) { const unsigned char M_SOF0 = 0xc0; const unsigned char M_SOF1 = 0xc1; const unsigned char M_SOF2 = 0xc2; const unsigned char M_SOF3 = 0xc3; const unsigned char M_SOF5 = 0xc5; const unsigned char M_SOF6 = 0xc6; const unsigned char M_SOF7 = 0xc7; const unsigned char M_SOF9 = 0xc9; const unsigned char M_SOF10 = 0xcA; const unsigned char M_SOF11 = 0xcb; const unsigned char M_SOF13 = 0xcd; const unsigned char M_SOF14 = 0xce; const unsigned char M_SOF15 = 0xcf; const unsigned char M_SOI = 0xd8; const unsigned char M_EOI = 0xd9; const unsigned char M_COM = 0xfe; union Byte { char c; unsigned char u; }; if (comment.empty() || ba.length() < 2) return; // first marker Byte a,b; a.c = ba[0]; b.c = ba[1]; if (a.u == 0xff && b.u == M_SOI) { int index = 2; int len = ba.length(); while (index < len) { // next marker a.c = ba[index++]; while (a.u != 0xff && index < len) { a.c = ba[index++]; } do { b.c = ba[index++]; } while (b.u == 0xff && index < len); switch (b.u) { case M_SOF0: case M_SOF1: case M_SOF2: case M_SOF3: case M_SOF5: case M_SOF6: case M_SOF7: case M_SOF9: case M_SOF10: case M_SOF11: case M_SOF13: case M_SOF14: case M_SOF15: case M_EOI: { Byte a, b; a.u = 0xff; b.u = M_COM; index -= 2; // insert comment before marker ba.insert(index++, a.c); ba.insert(index++, b.c); int val = comment.size() + 2; ba.insert(index++,(val >> 8) & 0xff); ba.insert(index++,val & 0xff); ba.insert(index, comment.c_str()); index = len; // finished } break; case M_COM: default: { Byte a, b; a.c = ba[index++]; b.c = ba[index++]; int off = ((unsigned int)a.u << 8) + (unsigned int)b.u; index += off; index -= 2; // next marker } break; } } } } // --------------------------------------------------------------- #define PRIVATE(p) p #define PUBLIC(p) p void SoQtOffscreenRenderer::init(const SbViewportRegion & vpr, SoGLRenderAction * glrenderaction) { this->backgroundcolor.setValue(0,0,0); if (glrenderaction) { this->renderaction = glrenderaction; } else { this->renderaction = new SoGLRenderAction(vpr); this->renderaction->setCacheContext(SoGLCacheContextElement::getUniqueCacheContext()); this->renderaction->setTransparencyType(SoGLRenderAction::SORTED_OBJECT_BLEND); } this->didallocation = glrenderaction ? false : true; this->viewport = vpr; this->pixelbuffer = NULL; // constructed later this->framebuffer = NULL; this->numSamples = -1; this->pbuffer = QGLPixelBuffer::hasOpenGLPbuffers(); } /*! Constructor. Argument is the \a viewportregion we should use when rendering. An internal SoGLRenderAction will be constructed. */ SoQtOffscreenRenderer::SoQtOffscreenRenderer(const SbViewportRegion & viewportregion) { init(viewportregion); } /*! Constructor. Argument is the \a action we should apply to the scene graph when rendering the scene. Information about the viewport is extracted from the \a action. */ SoQtOffscreenRenderer::SoQtOffscreenRenderer(SoGLRenderAction * action) { init(action->getViewportRegion(), action); } /*! Destructor. */ SoQtOffscreenRenderer::~SoQtOffscreenRenderer() { delete pixelbuffer; delete framebuffer; if (this->didallocation) { delete this->renderaction; } } /*! Sets the viewport region. This will invalidate the current buffer, if any. The buffer will not contain valid data until another call to SoOffscreenRendererQt::render() happens. */ void SoQtOffscreenRenderer::setViewportRegion(const SbViewportRegion & region) { PRIVATE(this)->viewport = region; } /*! Returns the viewerport region. */ const SbViewportRegion & SoQtOffscreenRenderer::getViewportRegion(void) const { return PRIVATE(this)->viewport; } /*! Sets the background color. The buffer is cleared to this color before rendering. */ void SoQtOffscreenRenderer::setBackgroundColor(const SbColor & color) { PRIVATE(this)->backgroundcolor = color; } /*! Returns the background color. */ const SbColor & SoQtOffscreenRenderer::getBackgroundColor(void) const { return PRIVATE(this)->backgroundcolor; } /*! Sets the render action. Use this if you have special rendering needs. */ void SoQtOffscreenRenderer::setGLRenderAction(SoGLRenderAction * action) { if (action == PRIVATE(this)->renderaction) { return; } if (PRIVATE(this)->didallocation) { delete PRIVATE(this)->renderaction; } PRIVATE(this)->renderaction = action; PRIVATE(this)->didallocation = false; } /*! Returns the rendering action currently used. */ SoGLRenderAction * SoQtOffscreenRenderer::getGLRenderAction(void) const { return PRIVATE(this)->renderaction; } void SoQtOffscreenRenderer::setNumPasses(const int num) { PRIVATE(this)->numSamples = num; } int SoQtOffscreenRenderer::getNumPasses(void) const { return PRIVATE(this)->numSamples; } void SoQtOffscreenRenderer::setPbufferEnable(SbBool enable) { PRIVATE(this)->pbuffer = enable; } SbBool SoQtOffscreenRenderer::getPbufferEnable(void) const { return PRIVATE(this)->pbuffer; } // ************************************************************************* void SoQtOffscreenRenderer::pre_render_cb(void * userdata, SoGLRenderAction * action) { glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT); action->setRenderingIsRemote(false); } void SoQtOffscreenRenderer::makePixelBuffer(int width, int height, int samples) { if (pixelbuffer) { delete pixelbuffer; pixelbuffer = NULL; } viewport.setWindowSize(width, height); QGLFormat fmt; if (samples > 0) { fmt.setSampleBuffers(true); fmt.setSamples(samples); } else { fmt.setSampleBuffers(false); } pixelbuffer = new QGLPixelBuffer(width, height, fmt); cache_context = SoGLCacheContextElement::getUniqueCacheContext(); // unique per pixel buffer object, just to be sure } void SoQtOffscreenRenderer::makeFrameBuffer(int width, int height, int samples) { if (framebuffer) { delete framebuffer; framebuffer = NULL; } viewport.setWindowSize(width, height); #if QT_VERSION >= 0x040600 QGLFramebufferObjectFormat fmt; fmt.setSamples(samples); fmt.setAttachment(QGLFramebufferObject::Depth); #else QGLFramebufferObject::Attachment fmt; fmt = QGLFramebufferObject::Depth; #endif framebuffer = new QGLFramebufferObject(width, height, fmt); cache_context = SoGLCacheContextElement::getUniqueCacheContext(); // unique per pixel buffer object, just to be sure } SbBool SoQtOffscreenRenderer::renderFromBase(SoBase * base) { const SbVec2s fullsize = this->viewport.getViewportSizePixels(); if (PRIVATE(this)->pbuffer) { if (!pixelbuffer) { makePixelBuffer(fullsize[0], fullsize[1], PRIVATE(this)->numSamples); } else if (pixelbuffer->width() != fullsize[0] || pixelbuffer->height() != fullsize[1]) { // get the size right! makePixelBuffer(fullsize[0], fullsize[1], PRIVATE(this)->numSamples); } pixelbuffer->makeCurrent(); // activate us! } else { if (!framebuffer) { makeFrameBuffer(fullsize[0], fullsize[1], PRIVATE(this)->numSamples); } else if (framebuffer->width() != fullsize[0] || framebuffer->height() != fullsize[1]) { // get the size right! makeFrameBuffer(fullsize[0], fullsize[1], PRIVATE(this)->numSamples); } framebuffer->bind(); // activate us! } // oldcontext is used to restore the previous context id, in case // the render action is not allocated by us. const uint32_t oldcontext = this->renderaction->getCacheContext(); this->renderaction->setCacheContext(cache_context); glEnable(GL_DEPTH_TEST); glClearColor(this->backgroundcolor[0], this->backgroundcolor[1], this->backgroundcolor[2], 0.0f); // needed to clear viewport after glViewport() is called from // SoGLRenderAction this->renderaction->addPreRenderCallback(pre_render_cb, NULL); this->renderaction->setViewportRegion(this->viewport); if (base->isOfType(SoNode::getClassTypeId())) this->renderaction->apply((SoNode *)base); else if (base->isOfType(SoPath::getClassTypeId())) this->renderaction->apply((SoPath *)base); else { assert(false && "Cannot apply to anything else than an SoNode or an SoPath"); } this->renderaction->removePreRenderCallback(pre_render_cb, NULL); if (PRIVATE(this)->pbuffer) { pixelbuffer->doneCurrent(); } else { framebuffer->release(); } this->renderaction->setCacheContext(oldcontext); // restore old return true; } /*! Render the scenegraph rooted at \a scene into our internal pixel buffer. Important note: make sure you pass in a \a scene node pointer which has both a camera and at least one lightsource below it -- otherwise you are likely to end up with just a blank or black image buffer. This mistake is easily made if you use an SoQtOffscreenRenderer on a scenegraph from one of the standard viewer components, as you will often just leave the addition of a camera and a headlight lightsource to the viewer to set up. This camera and lightsource are then part of the viewer's private "super-graph" outside of the scope of the scenegraph passed in by the application programmer. To make sure the complete scenegraph (including the viewer's "private parts" (*snicker*)) are passed to this method, you can get the scenegraph root from the viewer's internal SoSceneManager instance instead of from the viewer's own getSceneGraph() method, like this: \code SoQtOffscreenRenderer * myRenderer = new SoQtOffscreenRenderer(vpregion); SoNode * root = myViewer->getSceneManager()->getSceneGraph(); SbBool ok = myRenderer->render(root); // [then use image buffer in a texture, or write it to file, or whatever] \endcode If you do this and still get a blank buffer, another common problem is to have a camera which is not actually pointing at the scene geometry you want a snapshot of. If you suspect that could be the cause of problems on your end, take a look at SoCamera::pointAt() and SoCamera::viewAll() to see how you can make a camera node guaranteed to be directed at the scene geometry. Yet another common mistake when setting up the camera is to specify values for the SoCamera::nearDistance and SoCamera::farDistance fields which doesn't not enclose the full scene. This will result in either just the background color, or that parts at the front or the back of the scene will not be visible in the rendering. \sa writeToImage() */ SbBool SoQtOffscreenRenderer::render(SoNode * scene) { return PRIVATE(this)->renderFromBase(scene); } /*! Render the \a scene path into our internal memory buffer. */ SbBool SoQtOffscreenRenderer::render(SoPath * scene) { return PRIVATE(this)->renderFromBase(scene); } /*! Writes the rendered image buffer directly into a QImage object. */ void SoQtOffscreenRenderer::writeToImage (QImage& img) const { if (PRIVATE(this)->pbuffer) { if (pixelbuffer) img = pixelbuffer->toImage(); } else { if (framebuffer) img = framebuffer->toImage(); } } /*! This method returns all image file formats supported by Coin3D (see getWriteFiletypeInfo()) with all QImage file formats that are not directly supported by Coin3D, if so. */ QStringList SoQtOffscreenRenderer::getWriteImageFiletypeInfo() const { QList qtformats = QImageWriter::supportedImageFormats(); QStringList formats; for (QList::Iterator it = qtformats.begin(); it != qtformats.end(); ++it) { formats << QLatin1String(*it); } formats.sort(); return formats; } #undef PRIVATE #undef PUBLIC