/**************************************************************************** ** Copyright (c) 2013 Debao Zhang ** All right reserved. ** ** Permission is hereby granted, free of charge, to any person obtaining ** a copy of this software and associated documentation files (the ** "Software"), to deal in the Software without restriction, including ** without limitation the rights to use, copy, modify, merge, publish, ** distribute, sublicense, and/or sell copies of the Software, and to ** permit persons to whom the Software is furnished to do so, subject to ** the following conditions: ** ** The above copyright notice and this permission notice shall be ** included in all copies or substantial portions of the Software. ** ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ** NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE ** LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION ** OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION ** WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ** ****************************************************************************/ #include #include #include #include #include #include namespace { int writeToIODevice(GifFileType *gifFile, const GifByteType *data, int maxSize) { return static_cast( static_cast( gifFile->UserData)->write( reinterpret_cast(data), maxSize)); } int readFromIODevice(GifFileType *gifFile, GifByteType *data, int maxSize) { return static_cast( static_cast( gifFile->UserData)->read( reinterpret_cast(data), maxSize)); } } // namespace QGifImagePrivate::QGifImagePrivate(QGifImage *p) : loopCount(0), defaultDelayTime(1000), q_ptr(p) {} QVector QGifImagePrivate::colorTableFromColorMapObject( ColorMapObject *colorMap, int transColorIndex) const { QVector colorTable; if (colorMap) { for (int idx = 0; idx < colorMap->ColorCount; ++idx) { GifColorType gifColor = colorMap->Colors[idx]; QRgb color = gifColor.Blue | (gifColor.Green << 8) | (gifColor.Red << 16); // For non-transparent color, set the alpha to opaque. if (idx != transColorIndex) color |= 0xff << 24; colorTable.append(color); } } return colorTable; } ColorMapObject *QGifImagePrivate::colorTableToColorMapObject( QVector colorTable) const { if (colorTable.isEmpty()) return nullptr; auto cmap = (ColorMapObject *)malloc(sizeof(ColorMapObject)); // num of colors must be a power of 2 int numColors = 1 << GifBitSize(static_cast(colorTable.size())); cmap->ColorCount = numColors; // Maybe a bug of giflib, BitsPerPixel is used as size of the color table // size. cmap->BitsPerPixel = GifBitSize(static_cast(colorTable.size())); // Todo! cmap->SortFlag = false; auto colorValues = (GifColorType *)calloc(numColors, sizeof(GifColorType)); for (int idx = 0; idx < colorTable.size(); ++idx) { colorValues[idx].Red = qRed(colorTable[idx]); colorValues[idx].Green = qGreen(colorTable[idx]); colorValues[idx].Blue = qBlue(colorTable[idx]); } cmap->Colors = colorValues; return cmap; } QSize QGifImagePrivate::getCanvasSize() const { // If canvasSize has been set by user. if (canvasSize.isValid()) return canvasSize; // Calc the right canvasSize from the frame size. int width = -1; int height = -1; foreach (QGifFrameInfoData info, frameInfos) { int w = info.image.width() + info.offset.x(); int h = info.image.height() + info.offset.y(); if (w > width) width = w; if (h > height) height = h; } return {width, height}; } int QGifImagePrivate::getFrameTransparentColorIndex( const QGifFrameInfoData &frameInfo) const { int index = -1; QColor transColor = frameInfo.transparentColor.isValid() ? frameInfo.transparentColor : defaultTransparentColor; if (transColor.isValid()) { if (!frameInfo.image.colorTable().isEmpty()) index = static_cast(frameInfo.image.colorTable().indexOf(transColor.rgb())); else if (!globalColorTable.isEmpty()) index = static_cast(globalColorTable.indexOf(transColor.rgb())); } return index; } bool QGifImagePrivate::load(QIODevice *device) { static int interlacedOffset[] = {0, 4, 2, 1}; /* The way Interlaced image should. */ static int interlacedJumps[] = {8, 8, 4, 2}; /* be read - offsets and jumps... */ int error; GifFileType *gifFile = DGifOpen(device, readFromIODevice, &error); if (!gifFile) { char *temp = GifErrorString(error); qWarning("%s", temp); return false; } if (DGifSlurp(gifFile) == GIF_ERROR) return false; canvasSize.setWidth(gifFile->SWidth); canvasSize.setHeight(gifFile->SHeight); if (gifFile->SColorMap) { globalColorTable = colorTableFromColorMapObject(gifFile->SColorMap); if (gifFile->SBackGroundColor < globalColorTable.size()) bgColor = QColor(globalColorTable[gifFile->SBackGroundColor]); } for (int idx = 0; idx < gifFile->ImageCount; ++idx) { SavedImage gifImage = gifFile->SavedImages[idx]; int top = gifImage.ImageDesc.Top; int left = gifImage.ImageDesc.Left; int width = gifImage.ImageDesc.Width; int height = gifImage.ImageDesc.Height; QGifFrameInfoData frameInfo; GraphicsControlBlock gcb; DGifSavedExtensionToGCB(gifFile, idx, &gcb); int transColorIndex = gcb.TransparentColor; QVector colorTable; if (gifImage.ImageDesc.ColorMap) colorTable = colorTableFromColorMapObject(gifImage.ImageDesc.ColorMap, transColorIndex); else if (transColorIndex != -1) colorTable = colorTableFromColorMapObject(gifFile->SColorMap, transColorIndex); else colorTable = globalColorTable; if (transColorIndex != -1) frameInfo.transparentColor = colorTable[transColorIndex]; frameInfo.delayTime = gcb.DelayTime * 10; // convert to milliseconds frameInfo.interlace = gifImage.ImageDesc.Interlace; frameInfo.offset = QPoint(left, top); QImage image(width, height, QImage::Format_Indexed8); image.setOffset(QPoint(left, top)); // Maybe useful for some users. image.setColorTable(colorTable); if (transColorIndex != -1) image.fill(transColorIndex); else if (!globalColorTable.isEmpty()) image.fill(gifFile->SBackGroundColor); //! ToDo if (gifImage.ImageDesc.Interlace) { int line = 0; for (int i = 0; i < 4; i++) { for (int row = interlacedOffset[i]; row < height; row += interlacedJumps[i]) { memcpy(image.scanLine(row), gifImage.RasterBits + line * width, width); line++; } } } else { for (int row = 0; row < height; row++) { memcpy(image.scanLine(row), gifImage.RasterBits + row * width, width); } } // Extract other data for the image. if (idx == 0) { if (gifImage.ExtensionBlockCount > 2) { ExtensionBlock *extBlock = gifImage.ExtensionBlocks; if (extBlock->Function == APPLICATION_EXT_FUNC_CODE && extBlock->ByteCount == 8) { if (QByteArray((char *)extBlock->Bytes) == QByteArray("NETSCAPE2.0")) { ExtensionBlock *block = gifImage.ExtensionBlocks + 1; if (block->ByteCount == 3) { loopCount = uchar(block->Bytes[1]) + uchar((block->Bytes[2]) << 8); } } } } } frameInfo.image = image; frameInfos.append(frameInfo); } DGifCloseFile(gifFile); return true; } bool QGifImagePrivate::save(QIODevice *device) const { int error; GifFileType *gifFile = EGifOpen(device, writeToIODevice, &error); if (!gifFile) { char *temp = GifErrorString(error); qWarning("%s", temp); return false; } QSize _canvasSize = getCanvasSize(); gifFile->SWidth = _canvasSize.width(); gifFile->SHeight = _canvasSize.height(); gifFile->SColorResolution = 8; if (!globalColorTable.isEmpty()) { gifFile->SColorMap = colorTableToColorMapObject(globalColorTable); int idx = static_cast(globalColorTable.indexOf(bgColor.rgba())); gifFile->SBackGroundColor = idx == -1 ? 0 : idx; } gifFile->ImageCount = static_cast(frameInfos.size()); gifFile->SavedImages = (SavedImage *)calloc(frameInfos.size(), sizeof(SavedImage)); for (int idx = 0; idx < frameInfos.size(); ++idx) { const QGifFrameInfoData frameInfo = frameInfos.at(idx); QImage image = frameInfo.image; if (image.format() != QImage::Format_Indexed8) { if (!globalColorTable.isEmpty()) image = image.convertToFormat(QImage::Format_Indexed8, globalColorTable); else image = image.convertToFormat(QImage::Format_Indexed8); } SavedImage *gifImage = gifFile->SavedImages + idx; gifImage->ImageDesc.Left = frameInfo.offset.x(); gifImage->ImageDesc.Top = frameInfo.offset.y(); gifImage->ImageDesc.Width = image.width(); gifImage->ImageDesc.Height = image.height(); gifImage->ImageDesc.Interlace = frameInfo.interlace; if (!image.colorTable().isEmpty() && (image.colorTable() != globalColorTable)) gifImage->ImageDesc.ColorMap = colorTableToColorMapObject(image.colorTable()); else gifImage->ImageDesc.ColorMap = nullptr; auto data = (GifByteType *)malloc(image.width() * image.height() * sizeof(GifByteType)); for (int row = 0; row < image.height(); ++row) { memcpy(data + row * image.width(), image.scanLine(row), image.width()); } gifImage->RasterBits = data; if (idx == 0) { uchar data8[12] = "NETSCAPE2.0"; GifAddExtensionBlock(&gifImage->ExtensionBlockCount, &gifImage->ExtensionBlocks, APPLICATION_EXT_FUNC_CODE, 11, data8); uchar data_char[3]; data_char[0] = 0x01; data_char[1] = loopCount & 0xFF; data_char[2] = (loopCount >> 8) & 0xFF; GifAddExtensionBlock(&gifImage->ExtensionBlockCount, &gifImage->ExtensionBlocks, CONTINUE_EXT_FUNC_CODE, 3, data_char); } GraphicsControlBlock gcbBlock; gcbBlock.DisposalMode = 0; gcbBlock.UserInputFlag = false; gcbBlock.TransparentColor = getFrameTransparentColorIndex(frameInfo); if (frameInfo.delayTime != -1) gcbBlock.DelayTime = frameInfo.delayTime / 10; // convert from milliseconds else gcbBlock.DelayTime = defaultDelayTime / 10; EGifGCBToSavedExtension(&gcbBlock, gifFile, idx); } EGifSpew(gifFile); return true; } /*! \class QGifImage \inmodule QtGifImage \brief Class used to read/wirte .gif files. */ /*! Constructs a gif image */ QGifImage::QGifImage() : d_ptr(new QGifImagePrivate(this)) {} /*! Constructs a gif image and tries to load the image from the file with the given \a fileName */ QGifImage::QGifImage(const QString &fileName) : d_ptr(new QGifImagePrivate(this)) { load(fileName); } /*! Constructs a gif image with the given \a size */ QGifImage::QGifImage(const QSize &size) : d_ptr(new QGifImagePrivate(this)) { d_ptr->canvasSize = size; } /*! Destroys the gif image and cleans up. */ QGifImage::~QGifImage() { delete d_ptr; } /*! Return global color table. */ QVector QGifImage::globalColorTable() const { Q_D(const QGifImage); return d->globalColorTable; } /*! Return background color of the gif canvas. It only makes sense when global color table is not empty. */ QColor QGifImage::backgroundColor() const { Q_D(const QGifImage); return d->bgColor; } /*! Set the global color table \a colors and background color \a bgColor. \a bgColor must be one the color in \a colors. Unlike other image formats that support alpha (e.g. png), GIF does not support semi-transparent pixels. So the alpha channel of the color table will be ignored. */ void QGifImage::setGlobalColorTable(const QVector &colors, const QColor &bgColor) { Q_D(QGifImage); d->globalColorTable = colors; d->bgColor = bgColor; } /*! Return the default delay in milliseconds. The default value is 1000 ms. The time delay can be different for every frame. */ int QGifImage::defaultDelay() const { Q_D(const QGifImage); return d->defaultDelayTime; } /*! Set the default \a delay in milliseconds. */ void QGifImage::setDefaultDelay(int delay) { Q_D(QGifImage); d->defaultDelayTime = delay; } /*! Return the default transparent color. The transparent color can be different for every frame. */ QColor QGifImage::defaultTransparentColor() const { Q_D(const QGifImage); return d->defaultTransparentColor; } /*! Set the default transparent \a color. Unlike other image formats that support alpha (e.g. png), GIF does not support semi-transparent pixels. The way to achieve transparency is to set a color that will be transparent when rendering the GIF. So, if you set the transparent color to black, the black pixels in the gif file will be transparent. */ void QGifImage::setDefaultTransparentColor(const QColor &color) { Q_D(QGifImage); d->defaultTransparentColor = color; } /*! Return the loop count. */ int QGifImage::loopCount() const { Q_D(const QGifImage); return d->loopCount; } /*! Set the loop count. The default value of \a loop is 0, which means loop forever. */ void QGifImage::setLoopCount(int loop) { Q_D(QGifImage); d->loopCount = loop; } /*! Insert the QImage object \a frame at position \a index with \a delay. As gif file only support indexed image, so all the \a frame will be converted to the QImage::Format_Indexed8 format. Global color table will be used in the convertion if it has been set. QImage::offset() will be used when insert the QImage to the gif canvas. */ void QGifImage::insertFrame(int index, const QImage &frame, int delay) { Q_D(QGifImage); QGifFrameInfoData data; data.image = frame; data.delayTime = delay; data.offset = frame.offset(); d->frameInfos.insert(index, data); } /*! \overload Insert the QImage object \a frame at position \a index with the given \a offset and \a delay. As gif file only support indexed image, so all the \a frame will be converted to the QImage::Format_Indexed8 format. Global color table will be used in the convertion if it has been set. */ void QGifImage::insertFrame(int index, const QImage &frame, const QPoint &offset, int delay) { Q_D(QGifImage); QGifFrameInfoData data; data.image = frame; data.delayTime = delay; data.offset = offset; d->frameInfos.insert(index, data); } /*! Append the QImage object \a frame with \a delay. As gif file only support indexed image, so all the \a frame will be converted to the QImage::Format_Indexed8 format. Global color table will be used in the convertion if it has been set. QImage::offset() will be used when insert the QImage to the gif canvas. */ void QGifImage::addFrame(const QImage &frame, int delay) { Q_D(QGifImage); QGifFrameInfoData data; data.image = frame; data.delayTime = delay; data.offset = frame.offset(); d->frameInfos.append(data); } /*! \overload Append the QImage object \a frame with the given \a offset and \a delay. */ void QGifImage::addFrame(const QImage &frame, const QPoint &offset, int delay) { Q_D(QGifImage); QGifFrameInfoData data; data.image = frame; data.delayTime = delay; data.offset = offset; d->frameInfos.append(data); } /*! Return frame count contained in the gif file. */ int QGifImage::frameCount() const { Q_D(const QGifImage); return static_cast(d->frameInfos.count()); } /*! Return the image at \a index. */ QImage QGifImage::frame(int index) const { Q_D(const QGifImage); if (index < 0 || index >= d->frameInfos.size()) return {}; return d->frameInfos[index].image; } /*! Return the offset value of the frame at \a index */ QPoint QGifImage::frameOffset(int index) const { Q_D(const QGifImage); if (index < 0 || index >= d->frameInfos.size()) return {}; return d->frameInfos[index].offset; } /*! Set the \a offset value for the frame at \a index */ void QGifImage::setFrameOffset(int index, const QPoint &offset) { Q_D(QGifImage); if (index < 0 || index >= d->frameInfos.size()) return; d->frameInfos[index].offset = offset; } /*! Return the delay value of the frame at \a index */ int QGifImage::frameDelay(int index) const { Q_D(const QGifImage); if (index < 0 || index >= d->frameInfos.size()) return -1; return d->frameInfos[index].delayTime; } /*! Set the \a delay value for the frame at \a index */ void QGifImage::setFrameDelay(int index, int delay) { Q_D(QGifImage); if (index < 0 || index >= d->frameInfos.size()) return; d->frameInfos[index].delayTime = delay; } /*! Return the transparent color of the frame at \a index */ QColor QGifImage::frameTransparentColor(int index) const { Q_D(const QGifImage); if (index < 0 || index >= d->frameInfos.size()) return {}; return d->frameInfos[index].transparentColor; } /*! Sets the transparent \a color of the frame \a index. Unlike other image formats that support alpha (e.g. PNG), GIF does not support semi-transparent pixels. The way to achieve transparency is to set a color that will be transparent when rendering the GIF. So, if you set the transparent color to black, the black pixels in your gif file will be transparent. */ void QGifImage::setFrameTransparentColor(int index, const QColor &color) { Q_D(QGifImage); if (index < 0 || index >= d->frameInfos.size()) return; d->frameInfos[index].transparentColor = color; } /*! Saves the gif image to the file with the given \a fileName. Returns \c true if the image was successfully saved; otherwise returns \c false. */ bool QGifImage::save(const QString &fileName) const { Q_D(const QGifImage); QFile file(fileName); if (file.open(QIODevice::WriteOnly)) { bool res = d->save(&file); // d->~QGifImagePrivate(); // delete d; return res; } return false; } /*! \overload This function writes a QImage to the given \a device. */ bool QGifImage::save(QIODevice *device) const { Q_D(const QGifImage); if (device->openMode() | QIODevice::WriteOnly) return d->save(device); return false; } /*! Loads an gif image from the file with the given \a fileName. Returns \c true if the image was successfully loaded; otherwise invalidates the image and returns \c false. */ bool QGifImage::load(const QString &fileName) { Q_D(QGifImage); QFile file(fileName); if (file.open(QIODevice::ReadOnly)) return d->load(&file); return false; } /*! \overload This function reads a gif image from the given \a device. This can, for example, be used to load an image directly into a QByteArray. */ bool QGifImage::load(QIODevice *device) { Q_D(QGifImage); if (device->openMode() | QIODevice::ReadOnly) return d->load(device); return false; }