diff --git a/Source/3rdpart/QColorQuantizer.cpp b/Source/3rdpart/QColorQuantizer.cpp deleted file mode 100644 index 70d693836..000000000 --- a/Source/3rdpart/QColorQuantizer.cpp +++ /dev/null @@ -1,576 +0,0 @@ -/******************************************************************************** - Copyright 2004 Sjaak Priester - - This file is part of Tinter. - - Tinter is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - Tinter 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Tinter; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -********************************************************************************/ - -// QColorQuantizer -// -// Version 1.0 (c) 2004, Sjaak Priester, Amsterdam. -// mailto:sjaak@sjaakpriester.nl -#include "atlheaders.h" -#include <cassert> -#include "3rdpart/GdiplusH.h" -#include "QColorQuantizer.h" -using namespace Gdiplus; -QColorQuantizer::QColorQuantizer(void) -: - m_Message(0) -, m_bStop(false) -, m_pSource(NULL) -, m_nMaxColors(0) -,m_Mode(HalfTone) -{ -} - -QColorQuantizer::~QColorQuantizer(void) -{ -} - -void QColorQuantizer::MakeQuantized(Image * pSource, Mode mode, UINT nMaxColors) -{ - Stop(); - m_pSource = pSource; - m_Mode = mode; - m_nMaxColors = nMaxColors; - -// m_Message = message; -ThreadProc(this); - //m_pThread = ::AfxBeginThread(ThreadProc, this); -} - -void QColorQuantizer::Stop(void) -{ - /*if (m_pThread) - { - m_bStop = true; - ::WaitForSingleObject(m_pThread->m_hThread, INFINITE); - }*/ -} - -/* static */ -UINT QColorQuantizer::ThreadProc(LPVOID pParam) -{ - QColorQuantizer * pThis = (QColorQuantizer *) pParam; - - - /*Bitmap * pResult = */pThis->Calculate(); - - - - //pThis->Notify(pResult); - return 0; -} - -// called by ThreadProc -Bitmap * QColorQuantizer::Calculate(void) -{ - m_bStop = false; - return GetQuantized(m_pSource, m_Mode, m_nMaxColors); -} - -// called by ThreadProc -void QColorQuantizer::Notify(Bitmap * pResult) -{ - //if (pResult && m_pMsgWnd) m_pMsgWnd->PostMessage(m_Message, 0, (LPARAM) pResult); -} - -Bitmap * QColorQuantizer::GetQuantized(Image * pSource, Mode mode, UINT nMaxColors) -{ - UINT w = pSource->GetWidth(); - UINT h = pSource->GetHeight(); - - Bitmap * pSrcBitmap = NULL; - - // If necessary, convert source to 24 bpp RGB bitmap - if (pSource->GetPixelFormat() == PixelFormat24bppRGB) pSrcBitmap = (Bitmap *) pSource; - else - { - pSrcBitmap = new Bitmap(w, h, PixelFormat24bppRGB); - - Graphics g(pSrcBitmap); - if (g.DrawImage(pSource, 0, 0, w, h) != Ok) - { - delete pSrcBitmap; - return NULL; - } - } - - // Create 8 bpp indexed bitmap of the same size - Bitmap * pResult = new Bitmap(w, h, PixelFormat8bppIndexed); - - bool bSucceeded = false; - - switch (mode) - { - case HalfTone: - bSucceeded = QuantizeWebSafe(pSrcBitmap, pResult); - break; - case Octree: - bSucceeded = QuantizeOctree(pSrcBitmap, pResult, nMaxColors); - break; - default: - break; - } - - if (m_bStop || ! bSucceeded) - { - delete pResult; - pResult = NULL; - } - - if (pSource != pSrcBitmap) delete pSrcBitmap; - - return pResult; -} - -bool QColorQuantizer::QuantizeWebSafe(Bitmap * pSource , Bitmap * pDest) -{ - // It would be nice if we could simply draw an Image to an 8bpp Bitmap, - // but GDI+ doesn't support it. Apparently, when saving a GIF, the conversion - // is done by the encoder. - // - // Websafe color reduction simply uses the standard GDI/GDI+ halftone palette. - // Entries 40...255 are the websafe colors, arranged like this: - // - // 40 ff000000 - // 41 ff000033 - // 42 ff000066 - // 43 ff000099 - // 44 ff0000cc - // 45 ff0000ff - // 46 ff003300 - // 47 ff003333 - // ... - // 252 ffffff66 - // 253 ffffff99 - // 254 ffffffcc - // 255 ffffffff - // - // For the 216 (= 6^3) websafe colors, the color scale in each dimension - // is divided in six parts. - // Knowing that 6 * 43 = 258 = just a little bit more than 256, we derive the - // following formula to calculate the color index from a 3x8 RGB color: - // index = 40 + 6 * (6 * ((R + 1) / 43) + ((G + 1) / 43)) + ((B + 1) / 43) - - bool r = true; - - UINT w = pSource->GetWidth(); - UINT h = pSource->GetHeight(); - Rect rc(0, 0, w, h); - - BitmapData dataSource; - - // Lock bits on 3x8 source bitmap - if (pSource->LockBits(& rc, ImageLockModeRead, PixelFormat24bppRGB, & dataSource) == Ok) - { - BitmapData dataDest; - - // Lock bits on indexed destination bitmap - if (pDest->LockBits(& rc, ImageLockModeWrite, PixelFormat8bppIndexed, & dataDest) == Ok) - { - BYTE * pRowSource = (BYTE *) dataSource.Scan0; - UINT strideSource; - - if (dataSource.Stride > 0) strideSource = dataSource.Stride; - else - { - // Compensate for possible negative stride - pRowSource += h * dataSource.Stride; - strideSource = - dataSource.Stride; - } - - BYTE * pRowDest = (BYTE *) dataDest.Scan0; - UINT strideDest; - - if (dataDest.Stride > 0) strideDest = dataDest.Stride; - else - { - pRowDest += h * dataDest.Stride; - strideDest = - dataDest.Stride; - } - - for (UINT y = 0; y < h; y++) // For each row... - { - BYTE * pPixelSource = pRowSource; - BYTE * pPixelDest = pRowDest; - - for (UINT x = 0; x < w; x++) // ...for each pixel... - { - // ...gather color information from 3x8 source bitmap... - UINT b = * pPixelSource++; - UINT g = * pPixelSource++; - UINT r = * pPixelSource++; - - // ...calculate index in standard halftone palette... - UINT index = 40 + 6 * (6 * ((r + 1) / 43) + ((g + 1) / 43)) + ((b + 1) / 43); - - // ...and put result in indexed bitmap. - * pPixelDest++ = (BYTE) index; - } - - if (m_bStop) break; - - pRowSource += strideSource; - pRowDest += strideDest; - } - pDest->UnlockBits(& dataDest); - } - else r = false; - - pSource->UnlockBits(& dataSource); - } - else r = false; - - return r; -} - - -//==================== -// Octree Color quantizing - - -// The work horse for Octree color quantizing. -bool QColorQuantizer::QuantizeOctree(Bitmap * pSource, Bitmap * pDest, UINT nMaxColors) -{ - if (nMaxColors > 256 || nMaxColors < 8) return false; - - bool r = true; - UINT nLeafs = 0; // counter of leaf nodes in octree - - OctreeNode head(1, NULL, nLeafs); // head of list with reducible non-leaf nodes (dummy) - head.m_nLevel = 0; - OctreeNode root(7, NULL, nLeafs); // root of octree - - // Put tree root in reducible list. Root will always be the last node in the list, - // and also functions as list sentinel. - head.m_pNext = & root; - root.m_pPrev = & head; - - UINT w = pSource->GetWidth(); - UINT h = pSource->GetHeight(); - Rect rc(0, 0, w, h); - - BitmapData dataSource; - - // Lock bits on 3x8 source bitmap - if (pSource->LockBits(& rc, ImageLockModeRead, PixelFormat24bppRGB, & dataSource) == Ok) - { - BYTE * pScan0Source = (BYTE *) dataSource.Scan0; - UINT strideSource; - - if (dataSource.Stride > 0) strideSource = dataSource.Stride; - else - { - // Compensate for possible negative stride - // (not needed for first loop, but we have to do it - // for second loop anyway) - pScan0Source += h * dataSource.Stride; - strideSource = - dataSource.Stride; - } - - BYTE * pRowSource = pScan0Source; - - // First loop: gather color information, put it in tree leaves - for (UINT y = 0; y < h; y++) // For each row... - { - BYTE * pPixelSource = pRowSource; - - for (UINT x = 0; x < w; x++) // ...for each pixel... - { - UINT b = * pPixelSource++; - UINT g = * pPixelSource++; - UINT r = * pPixelSource++; - - root.SetColor(r, g, b, & head, nLeafs); - // ...add color to tree, make new leaf if necessary. - - while (nLeafs > nMaxColors) // If in the process too many leaves are created... - { - // ... then, one by one, pick a non-leaf node... - OctreeNode * pReducible = head.m_pNext; - assert(pReducible); - - if (m_bStop || ! pReducible->m_pNext) break; - pReducible->Remove(); // ...remove it from the list of reducibles... - pReducible->GetQuantized(nLeafs); // ...and reduce it to a leaf. - } - if (m_bStop) break; - - } - if (m_bStop) break; - - pRowSource += strideSource; - } - - if (! m_bStop) - { - // We now have the important colors in the pic. Put them in a palette - BYTE * pPaletteBytes = new BYTE[sizeof(ColorPalette) + (nLeafs - 1) * sizeof(ARGB)]; - { - ColorPalette * pPalette = (ColorPalette *) pPaletteBytes; - pPalette->Flags = PaletteFlagsHasAlpha; - pPalette->Count = nLeafs; - - UINT index = 0; - root.FillPalette(pPalette, index); // Let octree fill the palette - - assert(index == nLeafs); - - pDest->SetPalette(pPalette); - - BitmapData dataDest; - - // Lock bits on indexed destination bitmap - if (pDest->LockBits(& rc, ImageLockModeWrite, PixelFormat8bppIndexed, & dataDest) == Ok) - { - BYTE * pRowSource = pScan0Source; - - BYTE * pRowDest = (BYTE *) dataDest.Scan0; - UINT strideDest; - - // Compensate for possible negative stride - if (dataDest.Stride > 0) strideDest = dataDest.Stride; - else - { - pRowDest += h * dataDest.Stride; - strideDest = - dataDest.Stride; - } - - // Second loop: fill indexed bitmap - for (UINT y = 0; y < h; y++) // For each row... - { - BYTE * pPixelSource = pRowSource; - BYTE * pPixelDest = pRowDest; - - for (UINT x = 0; x < w; x++) // ...for each pixel... - { - // ...gather color information from source bitmap... - UINT b = * pPixelSource++; - UINT g = * pPixelSource++; - UINT r = * pPixelSource++; - - // ...let octree calculate index... - BYTE index = (BYTE) root.GetPaletteIndex(r, g, b); - - // ...and put index in the destination bitmap. - * pPixelDest++ = index; - } - - pRowSource += strideSource; - pRowDest += strideDest; - - if (m_bStop) break; - } - pDest->UnlockBits(& dataDest); - } - else r = false; - - delete[] pPaletteBytes; - } - //else r = false; - } - pSource->UnlockBits(& dataSource); - } - else r = false; - if (m_bStop) r = false; - - return r; -} - -//=============== -// OctreeNode helper class - -QColorQuantizer::OctreeNode::OctreeNode(UINT level, OctreeNode * pHead, UINT& nLeafs) -: m_nPixels(0) -, m_totalR(0) -, m_totalG(0) -, m_totalB(0) -, m_nLevel(level) -, m_bLeaf(level == 0) -, m_pNext(NULL) -, m_pPrev(NULL) -{ - for (UINT i = 0; i < 8; i++) m_pChild[i] = NULL; - - if (m_bLeaf) ++nLeafs; // If we are a leaf, increment counter - else if (pHead) - { - // Otherwise, put us at front of list of reducible non-leaf nodes... - InsertAfter(pHead); - - // ...and determine our priority when it comes to reducing. - UpdatePosition(); - } -} - -QColorQuantizer::OctreeNode::~OctreeNode() -{ - for (UINT i = 0; i < 8; i++) delete m_pChild[i]; -} - -void QColorQuantizer::OctreeNode::UpdatePosition() -{ - // Determine our priority, in other words our position in the list. - assert(m_pNext); - assert(m_pPrev); - - OctreeNode * pNext = m_pNext; - - // Increment as long as pNext should be reduced before we are. - while (pNext && ! ReduceBefore(* pNext)) pNext = pNext->m_pNext; - assert(pNext); - - if (pNext != m_pNext) - { - Remove(); // Remove us from old position in list... - InsertBefore(pNext); // ...and insert us at new position. - } -} - -void QColorQuantizer::OctreeNode::Remove() -{ - assert(m_pNext); - assert(m_pPrev); - - m_pPrev->m_pNext = m_pNext; - m_pNext->m_pPrev = m_pPrev; -} - -void QColorQuantizer::OctreeNode::InsertBefore(OctreeNode * pNext) -{ - assert(pNext); - - m_pPrev = pNext->m_pPrev; - m_pNext = pNext; - - m_pPrev->m_pNext = this; - m_pNext->m_pPrev = this; -} - -UINT QColorQuantizer::OctreeNode::ChildIndex(UINT r, UINT g, UINT b) const -{ - // The octree trick: calculate child index based on the m_Level'th bits - // of the color bytes. - UINT rBit = (r >> m_nLevel) & 1; - UINT gBit = (g >> m_nLevel) & 1; - UINT bBit = (b >> m_nLevel) & 1; - - rBit <<= 2; - rBit += gBit << 1; - rBit += bBit; - - return rBit; -} - -void QColorQuantizer::OctreeNode::SetColor(UINT r, UINT g, UINT b, OctreeNode * pHead, UINT& nLeafs) -{ - m_nPixels++; - if (m_bLeaf) - { - // If we are a leaf node, accumulate color bytes in totals. - m_totalR += r; - m_totalG += g; - m_totalB += b; - } - else - { - if (m_pNext) UpdatePosition(); // Perhaps we should move further down the list, - // because we represent more pixels. - - // We are not a leaf node, so delegate to one of our children. - UINT index = ChildIndex(r, g, b); - - // If we don't have a matching child, create it. - if (m_pChild[index] == NULL) - m_pChild[index] = new OctreeNode(m_nLevel - 1, pHead, nLeafs); - - m_pChild[index]->SetColor(r, g, b, pHead, nLeafs); // recursion - } -} - -UINT QColorQuantizer::OctreeNode::GetPaletteIndex(UINT r, UINT g, UINT b) const -{ - // If we are a leaf node, the palette index is simply stored. - if (m_bLeaf) return m_iPaletteIndex; - else - { - // Otherwise, the matching child node must know more about it. - UINT index = ChildIndex(r, g, b); - assert(m_pChild[index] != NULL); - return m_pChild[index]->GetPaletteIndex(r, g, b); // recursion - } -} - -void QColorQuantizer::OctreeNode::GetQuantized(UINT& nLeafs) -{ - // GetQuantized a non-leaf node to a leaf - assert(! m_bLeaf); - - UINT r = 0; - UINT g = 0; - UINT b = 0; - - for (UINT i = 0; i < 8; i++) - { - OctreeNode * pChild = m_pChild[i]; - if (pChild == NULL) continue; - - assert(pChild->m_bLeaf); - - // Accumulate color totals of our children... - r += pChild->m_totalR; - g += pChild->m_totalG; - b += pChild->m_totalB; - - // ...and delete them. - delete pChild; - m_pChild[i] = NULL; - - --nLeafs; - } - m_totalR = r; - m_totalG = g; - m_totalB = b; - - // Now we have become a leaf node ourselves. - m_bLeaf = true; - ++nLeafs; -} - -void QColorQuantizer::OctreeNode::FillPalette(ColorPalette * pPal, UINT& index) -{ - if (m_bLeaf) - { - // If we are a leaf, calculate the color by dividing the color totals - // through the number of pixels we represent... - Color col(255, - (BYTE)(m_totalR / m_nPixels), - (BYTE)(m_totalG / m_nPixels), - (BYTE)(m_totalB / m_nPixels)); - - // ...and put it in the palette. - pPal->Entries[index] = col.GetValue(); - m_iPaletteIndex = index++; - } - else - { - // Otherwise, ask our child nodes. - for (UINT i = 0; i < 8; i++) - if (m_pChild[i] != NULL) m_pChild[i]->FillPalette(pPal, index); // recursion - } -} diff --git a/Source/3rdpart/QColorQuantizer.h b/Source/3rdpart/QColorQuantizer.h deleted file mode 100644 index a93bbef0b..000000000 --- a/Source/3rdpart/QColorQuantizer.h +++ /dev/null @@ -1,146 +0,0 @@ -/******************************************************************************** - Copyright 2004 Sjaak Priester - - This file is part of Tinter. - - Tinter is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - Tinter 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Tinter; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -********************************************************************************/ - -// QColorQuantizer -// -// MFC class to reduce colors of GDI+ Gdiplus::Bitmap, using the Octree algorithm or -// the Median Cut algorithm. -// Use at your own risk. Comments welcome. -// -// Version 1.0 (c) 2004, Sjaak Priester, Amsterdam. -// mailto:sjaak@sjaakpriester.nl - -#pragma once - -#pragma warning (disable: 4201) // nameless struct/union - -// Special windows message. -// After MakeQuantized() is finished, QColorQuantizer posts this message to the -// associated window pMsgWnd. -// LPARAM contains a Gdiplus::Bitmap *, pointing to a new 8 bpp indixed Gdiplus::Bitmap. The window -// gets ownership of the Gdiplus::Bitmap and is responsible for it's destruction. - -class QColorQuantizer -{ -public: - enum Mode - { - HalfTone, - WebSafe = HalfTone, - Octree - }; - QColorQuantizer(void); - virtual ~QColorQuantizer(void); - - Gdiplus::Bitmap * GetQuantized(Gdiplus::Image * pSource, Mode mode, UINT nMaxColors = 256); - // Return an 8bpp indexed Gdiplus::Bitmap, based on pSource. - // - // Parameters: - // - pSource points to source image, not necessarily Gdiplus::Bitmap; - // - mode one of the values of enum Mode; - // - nMaxColors maximum number of colors in returned Gdiplus::Bitmap. - // - // Return: - // - non-NULL new 8bpp indexed Gdiplus::Bitmap, user takes ownership and should delete; - // - NULL something went wrong, probably lack of memory. - // - // Remarks for different modes: - // - WebSafe, HalfTone Two names for the same mode. Returned Gdiplus::Bitmap has 216 colors of the - // GDI/GDI+ standard halftone palette. nMaxColors not used. Ultrafast. - // - Octree nMaxColors >= 8 and nMaxColors <= 256. Returned Gdiplus::Bitmap might have - // less than nMaxColors. Good result, relatively fast. - - void MakeQuantized(Gdiplus::Image * pSource, Mode mode, UINT nMaxColors = 256); - // Creates an 8bpp indexed Gdiplus::Bitmap in a separate working thread. Returns immediately. - // - // Parameters: - // - pSource points to source image, not necessarily Gdiplus::Bitmap; - // - mode one of the values of enum Mode; - // - pMsgWnd points to CWnd that will receive message when Gdiplus::Bitmap is completed; - // - nMaxColors maximum number of colors in returned Gdiplus::Bitmap; - // - message Windows message sent to pMsgWnd. - - void Stop(void); // Stop MakeQuantized's working thread. -// bool IsCalculating() { return m_pThread != NULL; } - -protected: - bool QuantizeWebSafe(Gdiplus::Bitmap * pSource , Gdiplus::Bitmap * pDest); - bool QuantizeOctree(Gdiplus::Bitmap * pSource, Gdiplus::Bitmap * pDest, UINT nMaxColors = 256); - // Worker functions GetQuantized() delegates to. - // - pDest should point to a new 8 bpp indexed Gdiplus::Bitmap with size of source. - - // Helper class for QuantizeOctree - class OctreeNode - { - public: - OctreeNode(UINT level, OctreeNode * pHead, UINT& nLeafs); - ~OctreeNode(); - void SetColor(UINT r, UINT g, UINT b, OctreeNode * pHead, UINT& nLeafs); - void GetQuantized(UINT& nLeafs); - UINT ChildIndex(UINT r, UINT g, UINT b) const; - UINT GetPaletteIndex(UINT r, UINT g, UINT b) const; - void FillPalette(Gdiplus::ColorPalette * pPal, UINT& index); - - bool ReduceBefore(const OctreeNode& other) const - { - // true if other should be reduced before we are - if (m_nLevel == other.m_nLevel) return m_nPixels < other.m_nPixels; - return m_nLevel < other.m_nLevel; - } - void UpdatePosition(); - void Remove(); - void InsertBefore(OctreeNode * pNext); - void InsertAfter(OctreeNode * pPrev) { InsertBefore(pPrev->m_pNext); } - - bool m_bLeaf; - UINT m_nLevel; - OctreeNode * m_pChild[8]; // pointers in octree - union // spare some memory by declaring variables not used together as union - { - UINT m_nPixels; - UINT m_iPaletteIndex; // only used in leaf, after FillPalette() - }; - union - { - struct - { - OctreeNode * m_pNext; // pointers in priority list, - OctreeNode * m_pPrev; // only used in non-leaf nodes - }; - struct - { - UINT m_totalR; // color totals, only used in leaf nodes - UINT m_totalG; - UINT m_totalB; - }; - }; - }; - -protected: - Gdiplus::Bitmap * Calculate(void); - void Notify(Gdiplus::Bitmap * pResult); - bool m_bStop; - UINT m_Message; - Gdiplus::Image *m_pSource; - Mode m_Mode; - UINT m_nMaxColors; - - static UINT ThreadProc(LPVOID pParam); -}; diff --git a/Source/Core/CMakeLists.txt b/Source/Core/CMakeLists.txt index 8aec66267..2db9575e6 100644 --- a/Source/Core/CMakeLists.txt +++ b/Source/Core/CMakeLists.txt @@ -202,8 +202,9 @@ if(WIN32) Images/WebpImageReader.cpp Images/HeifImageReader.cpp Images/ImageLoader.cpp - ../3rdpart/QColorQuantizer.cpp Images/GdiPlusImage.cpp + Images/ColorQuantizer.cpp + Images/OctreeColorQuantizer.cpp ../Func/GdiPlusInitializer.cpp ) @@ -236,7 +237,8 @@ if(WIN32) Images/HeifImageReader.h Images/AbstractImageReader.h Images/ImageLoader.h - ../3rdpart/QColorQuantizer.h + Images/ColorQuantizer.h + Images/OctreeColorQuantizer.h ) else() list(APPEND SRC_LIST diff --git a/Source/Core/Images/ColorQuantizer.cpp b/Source/Core/Images/ColorQuantizer.cpp new file mode 100644 index 000000000..8d1a4f54b --- /dev/null +++ b/Source/Core/Images/ColorQuantizer.cpp @@ -0,0 +1,91 @@ +#include "ColorQuantizer.h" + +#include "OctreeColorQuantizer.h" + +using namespace Gdiplus; + +std::unique_ptr<Bitmap> ColorQuantizer::getQuantized(Bitmap* sourceBitmap, Gdiplus::Color backgroundColor, UINT nMaxColors /* = 256 */) +{ + BYTE alphaThreshold = 0; + UINT width = sourceBitmap->GetWidth(); + UINT height = sourceBitmap->GetHeight(); + Rect rc(0, 0, width, height); + std::unique_ptr<Bitmap> destBitmap; + BitmapData dataSource; + + if (sourceBitmap->LockBits(&rc, ImageLockModeRead, PixelFormat32bppARGB, &dataSource) == Ok) { + BYTE* pScan0Source = (BYTE*)dataSource.Scan0; + UINT strideSource; + + if (dataSource.Stride > 0) { + strideSource = dataSource.Stride; + } else { + pScan0Source += height * dataSource.Stride; + strideSource = -dataSource.Stride; + } + + BYTE* pRowSource = pScan0Source; + OctreeColorQuantizer quantizer(nMaxColors, 8, &dataSource); + destBitmap = std::make_unique<Bitmap>(width, height, PixelFormat8bppIndexed); + + for (UINT y = 0; y < height; y++) { + BYTE* pPixelSource = pRowSource; + + for (UINT x = 0; x < width; x++) { + BYTE b = *pPixelSource++; + BYTE g = *pPixelSource++; + BYTE r = *pPixelSource++; + BYTE a = *pPixelSource++; + Gdiplus::Color c(a, r, g, b); + if (c.GetA() != 255) { + c = c.GetA() < alphaThreshold ? Color() : BlendWithBackgroundSrgb(c, backgroundColor); + } + + quantizer.addColor(c); + } + + pRowSource += strideSource; + } + + auto pallete = quantizer.generatePalette(); + MyPalette mypal(pallete.get(), backgroundColor, alphaThreshold); + + BitmapData dataDest; + destBitmap->SetPalette(pallete.get()); + if (destBitmap->LockBits(&rc, ImageLockModeWrite, PixelFormat8bppIndexed, &dataDest) == Ok) { + BYTE* pRowSource = pScan0Source; + + BYTE* pRowDest = (BYTE*)dataDest.Scan0; + UINT strideDest; + + if (dataDest.Stride > 0) { + strideDest = dataDest.Stride; + } else { + pRowDest += height * dataDest.Stride; + strideDest = -dataDest.Stride; + } + + for (UINT y = 0; y < height; y++) { + BYTE* pPixelSource = pRowSource; + BYTE* pPixelDest = pRowDest; + + for (UINT x = 0; x < width; x++) { + BYTE b = *pPixelSource++; + BYTE g = *pPixelSource++; + BYTE r = *pPixelSource++; + BYTE a = *pPixelSource++; + + BYTE index = (BYTE)mypal.getNearestColorIndex(Gdiplus::Color(a, r, g, b)); + + *pPixelDest++ = index; + } + + pRowSource += strideSource; + pRowDest += strideDest; + } + destBitmap->UnlockBits(&dataDest); + } + sourceBitmap->UnlockBits(&dataSource); + } + return destBitmap; +} diff --git a/Source/Core/Images/ColorQuantizer.h b/Source/Core/Images/ColorQuantizer.h new file mode 100644 index 000000000..4b40831b9 --- /dev/null +++ b/Source/Core/Images/ColorQuantizer.h @@ -0,0 +1,11 @@ + +#pragma once +#include <memory> + +#include <windows.h> +#include "3rdpart/GdiplusH.h" + +class ColorQuantizer { +public: + std::unique_ptr<Gdiplus::Bitmap> getQuantized(Gdiplus::Bitmap* pSource, Gdiplus::Color backgroundColor, UINT nMaxColors = 256); +}; diff --git a/Source/Core/Images/OctreeColorQuantizer.cpp b/Source/Core/Images/OctreeColorQuantizer.cpp new file mode 100644 index 000000000..09c444b8d --- /dev/null +++ b/Source/Core/Images/OctreeColorQuantizer.cpp @@ -0,0 +1,362 @@ +#include "OctreeColorQuantizer.h" + +int ToBitsPerPixel(int colorCount) { + if (colorCount == 1) { + return 1; + } + + // Bits per pixel is actually ceiling of log2(maxColors) + int bpp = 0; + for (int n = colorCount - 1; n > 0; n >>= 1) { + bpp++; + } + + return bpp; +} + +BYTE GetBrightness(Gdiplus::Color c) { + const float RLumSrgb = 0.299f; + const float GLumSrgb = 0.587f; + const float BLumSrgb = 0.114f; + + if (c.GetR() == c.GetG() && c.GetR() == c.GetB()) + return c.GetR(); + + return c.GetR() * RLumSrgb + c.GetG() * GLumSrgb + c.GetB() * BLumSrgb; +} + +Gdiplus::Color BlendWithBackgroundSrgb(Gdiplus::Color c, Gdiplus::Color backColor) { + assert(c.GetA() != 255); + assert(backColor.GetA() == 255); + + // The blending is applied only to the color and not the resulting alpha, which will always be opaque + if (c.GetA() == 0) + return backColor; + int inverseAlpha = 255 - c.GetA(); + + // The non-accelerated version. Division, eg. r:(byte)((c.R * c.A + backColor.R * inverseAlpha) / 255) would be more accurate + // but unlike for premultiplication we use the faster bit shifting because there is no inverse operation for blending. + return Gdiplus::Color(255, + (BYTE)((c.GetR() * c.GetA() + backColor.GetR() * inverseAlpha) >> 8), + (BYTE)((c.GetG() * c.GetA() + backColor.GetG() * inverseAlpha) >> 8), + (BYTE)((c.GetB() * c.GetA() + backColor.GetB() * inverseAlpha) >> 8)); +} + +MyPalette::MyPalette(Gdiplus::ColorPalette* palette, Gdiplus::Color backColor, BYTE alphaThreshold /*= 128*/) + : palette_(palette) + , backColor_(backColor) + , alphaThreshold_(alphaThreshold) +{ + transparentIndex_ = -1; + isGrayscale_ = true; + for (int i = 0; i < palette->Count; i++) { + Gdiplus::Color c = palette->Entries[i]; + if (!colorToIndex_.count(c.GetValue()) && !(alphaThreshold_ == 0 && c.GetA() == 0 && !hasMultiLevelAlpha_)) { + colorToIndex_[c.GetValue()] = i; + } + + if (c.GetA() != 255) { + hasAlpha_ = true; + if (!hasMultiLevelAlpha_) { + hasMultiLevelAlpha_ = c.GetA() > 0; + } + + if (c.GetA() == 0) { + if (transparentIndex_ < 0) + transparentIndex_ = i; + continue; + } + } + + if (isGrayscale_) { + isGrayscale_ = c.GetR() == c.GetG() && c.GetR() == c.GetB(); + } + } +} + +int MyPalette::findNearestColorIndexSrgb(Gdiplus::Color color) { + assert(!hasMultiLevelAlpha_); + assert(color.GetA() >= alphaThreshold_ || !(transparentIndex_ >= 0)); + + int minDiff = INT_MAX; + int resultIndex = 0; + + if (color.GetA() != 255) { + // blending the color with background and checking if there is an exact match now + color = BlendWithBackgroundSrgb(color, backColor_); + auto it = colorToIndex_.find(color.GetValue()); + if (it != colorToIndex_.end()) + return it->second; + } + + // The two similar lookups could be merged, but it is faster to separate them even if some parts are duplicated + int len = palette_->Count; + if (isGrayscale_) { + BYTE brightness = GetBrightness(color); + for (int i = 0; i < len; i++) { + Gdiplus::Color current = palette_->Entries[i]; + + // Palette color with alpha + if (current.GetA() != 255) { + // Skipping fully transparent palette colors because they were handled in GetNearestColorIndex + continue; + } + + // If the palette is grayscale, then distance is measured by perceived brightness. + int diff = std::abs(GetBrightness(current) - brightness); + if (diff >= minDiff) + continue; + + // new closest match + if (diff == 0) + return i; + minDiff = diff; + resultIndex = i; + } + } else { + + for (int i = 0; i < len; i++) { + Gdiplus::Color current = palette_->Entries[i]; + + // Palette color with alpha + if (current.GetA() != 255) { + // Skipping fully transparent palette colors because they were handled in GetNearestColorIndex + //Debug.Assert(current.A == 0, $ "If palette has partially transparent entries the {nameof(FindNearestColorIndexAlphaSrgb)} method should be used"); + continue; + } + + // If the palette is not grayscale, then distance is measured by Manhattan distance based on RGB components. + // Euclidean distance squared would provide a slightly different result in some cases but there is no good accelerated + // version for it using integers (DotProduct is available for floating point arguments only) + int diff; + + { + diff = std::abs(current.GetR() - color.GetR()) + std::abs(current.GetG() - color.GetG()) + std::abs(current.GetB() - color.GetB()); + } + + assert(diff != 0); + if (diff >= minDiff) + continue; + + // new closest match + minDiff = diff; + resultIndex = i; + } + } + + return resultIndex; +} + +int MyPalette::getNearestColorIndex(Gdiplus::Color c) { + // mapping alpha to full transparency + if (c.GetA() < alphaThreshold_ && transparentIndex_ >= 0) + return transparentIndex_; + + // exact match: from the palette + auto it = colorToIndex_.find(c.GetValue()); + if (it != colorToIndex_.end()) { + return it->second; + } + + return findNearestColorIndexSrgb(c); +} + +OctreeColorQuantizer::OctreeNode::~OctreeNode() +{ + for (auto* child: children_) { + delete child; + } +} + +int OctreeColorQuantizer::OctreeNode::deepPixelCount() +{ + int result = pixelCount_; + /* if (children == null) + return result;*/ + + // Adding also the direct children because reducing the tree starts at level levelCount - 2. + // And due to reducing no more than two levels can have non-empty nodes. + for (int index = 0; index < 8; index++) { + OctreeNode* node = children_[index]; + if (node != nullptr) + result += node->pixelCount_; + } + + return result; +} + +OctreeColorQuantizer::OctreeNode::OctreeNode(int level, OctreeColorQuantizer* parent) { + this->parent_ = parent; + assert(level < parent->levelCount_); + + if (level >= 0) { + parent->levels_[level].push_back(this); + } +} + +bool OctreeColorQuantizer::OctreeNode::addColor(Gdiplus::Color color, int level) { + // In the populating phase all colors are summed up in leaves at deepest level. + if (level == parent_->levelCount_) { + sumRed_ += color.GetR(); + sumGreen_ += color.GetG(); + sumBlue_ += color.GetB(); + pixelCount_++; + + // returning whether a new leaf has been added + return pixelCount_ == 1; + } + + assert(level < parent_->levelCount_); + + // Generating a 0..7 index based on the color components and adding new branches on demand. + int mask = 128 >> level; + int branchIndex = ((color.GetR() & mask) == mask ? 4 : 0) + | ((color.GetG() & mask) == mask ? 2 : 0) + | ((color.GetB() & mask) == mask ? 1 : 0); + OctreeNode* child = children_[branchIndex]; + + if (!child) { + child = new OctreeNode(level, parent_); + } + children_[branchIndex] = child; + return child->addColor(color, level + 1); +} + +void OctreeColorQuantizer::OctreeNode::mergeNodes(int& leavesCount) { + auto CompareByBrightness = [](OctreeNode* a, OctreeNode* b) -> bool { + if (a == nullptr || b == nullptr) + return a == b ? false : a == nullptr ? true + : false; + + auto ca = a->toColor(); + auto cb = b->toColor(); + return GetBrightness(ca) < GetBrightness(cb); + }; + + auto CompareByWeightedBrightness = [this](OctreeNode* a, OctreeNode* b) -> bool { + if (a == nullptr || b == nullptr) + return a == b ? false : a == nullptr ? true + : false; + + auto ca = a->toColor(); + auto cb = b->toColor(); + return GetBrightness(ca) * (a->deepPixelCount() / (float)parent_->size_) < GetBrightness(cb) * (b->deepPixelCount() / (float)parent_->size_); + }; + + /* if (children == null) + return;*/ + + // If there are fewer than 8 removals left we sort them to merge the least relevant ones first. + // For 2 colors (and 3 + TR) the distance is measured purely by brightness to avoid returning very similar colors. + // Note: reordering children is not a problem because we don't add more colors in merging phase. + if (parent_->colorCount() - parent_->maxColors_ < 8) { + auto cc = parent_->maxColors_ - (parent_->hasTransparency_ ? 1 : 0); + if (cc <= 2) + std::sort(std::begin(children_), std::end(children_), CompareByBrightness); + else + std::sort(std::begin(children_), std::end(children_), CompareByWeightedBrightness); + } + for (int i = 0; i < 8; i++) { + OctreeNode* node = children_[i]; + if (node == nullptr) + continue; + + assert(!node->isEmpty()); + + // Decreasing only if this node is not becoming a "leaf" while cutting a branch down. + if (!isEmpty()) { + leavesCount--; + } + + sumRed_ += node->sumRed_; + sumGreen_ += node->sumGreen_; + sumBlue_ += node->sumBlue_; + pixelCount_ += node->pixelCount_; + + children_[i] = nullptr; + delete node; + + // As we can return before merging all children, + // leavesCount may include "not-quite leaf" elements in the end. + if (parent_->colorCount() == parent_->maxColors_) + return; + } +} + +void OctreeColorQuantizer::OctreeNode::populatePalette(Gdiplus::Color* result, int& palIndex, int& remainingColors) { + // if a non-empty node is found, adding it to the resulting palette + if (!isEmpty()) { + result[palIndex] = toColor(); + palIndex += 1; + remainingColors -= 1; + if (remainingColors == 0) + return; + } + + /* if (children == null || context.IsCancellationRequested) + return;*/ + + for (OctreeNode* child : children_) { + if (child == nullptr) + continue; + child->populatePalette(result, palIndex, remainingColors); + if (remainingColors == 0) + return; + } +} + +OctreeColorQuantizer::OctreeColorQuantizer(int requestedColors, BYTE bitLevel, Gdiplus::BitmapData* source) { + maxColors_ = requestedColors; + size_ = source->Width * source->Height; + + levelCount_ = bitLevel ? bitLevel : std::min<>(8, ToBitsPerPixel(requestedColors)); + levels_.resize(levelCount_); + + root_ = std::make_unique<OctreeNode>(-1, this); +} + +unique_c_ptr<Gdiplus::ColorPalette> OctreeColorQuantizer::generatePalette() { + if (colorCount() > maxColors_) + reduceTree(); + + assert(colorCount() <= maxColors_); + //BYTE* result = new BYTE[sizeof(Gdiplus::ColorPalette) + leavesCount]; + auto pal = make_unique_malloc<Gdiplus::ColorPalette>(sizeof(Gdiplus::ColorPalette) + leavesCount_ * sizeof(Gdiplus::ARGB)); + pal->Count = leavesCount_; + pal->Flags = Gdiplus::PaletteFlagsHasAlpha; + + if (leavesCount_ > 0) { + int palIndex = 0; + root_->populatePalette(reinterpret_cast<Gdiplus::Color*>(pal->Entries), palIndex, leavesCount_); + assert(leavesCount_ == 0); + } + + // If transparent color is needed, then it will be automatically the last color in the result + return pal; +} + +void OctreeColorQuantizer::reduceTree() { + // Scanning all levels towards root. Leaves are skipped (hence -2) because they are not reducible. + for (int level = levelCount_ - 2; level >= 0; level--) { + if (levels_[level].size() == 0) + continue; + + // Sorting nodes of the current level (least significant ones first) + // while merging them into their parents until we go under MaxColors + auto& nodes = levels_[level]; + std::sort(nodes.begin(), nodes.end(), [](auto* a, auto* b) { return a->deepPixelCount() < b->deepPixelCount(); }); + //nodes.Sort((a, b) = > a.DeepPixelCount - b.DeepPixelCount); + + for (OctreeNode* node : nodes) { + // As merging is stopped when we reach MaxColors. + // leavesCount may include some half-merged non-leaf nodes as well. + node->mergeNodes(leavesCount_); + if (colorCount() <= maxColors_) + return; + } + } + + // If we are here, we need to reduce also the root node (less than 8 colors or 8 colors + transparency) + root_->mergeNodes(leavesCount_); + assert(colorCount() == maxColors_); +} diff --git a/Source/Core/Images/OctreeColorQuantizer.h b/Source/Core/Images/OctreeColorQuantizer.h new file mode 100644 index 000000000..703177cfa --- /dev/null +++ b/Source/Core/Images/OctreeColorQuantizer.h @@ -0,0 +1,101 @@ +// This class section was inspired by KGySoft.Drawing +// https://github.com/koszeggy/KGySoft.Drawing, which is under the KGy SOFT License. +// Original source code: https://github.com/koszeggy/KGySoft.Drawing/blob/master/KGySoft.Drawing.Core/Drawing/Imaging/_Quantizers/OptimizedPaletteQuantizer.Octree.cs +// The KGy SOFT License is available at https://github.com/koszeggy/KGySoft.Drawing/blob/master/LICENSE + +#pragma once +#include <algorithm> +#include <vector> +#include <cassert> +#include <memory> +#include <unordered_map> + +#include <windows.h> + +#include "3rdpart/GdiplusH.h" +#include "Core/Utils/CoreUtils.h" + +Gdiplus::Color BlendWithBackgroundSrgb(Gdiplus::Color c, Gdiplus::Color backColor); + +class MyPalette { +public: + explicit MyPalette(Gdiplus::ColorPalette* palette, Gdiplus::Color backColor, BYTE alphaThreshold = 128); + int findNearestColorIndexSrgb(Gdiplus::Color color); + int getNearestColorIndex(Gdiplus::Color c); +private: + Gdiplus::ColorPalette* palette_; + std::unordered_map<Gdiplus::ARGB, int> colorToIndex_; + Gdiplus::Color backColor_; + BYTE alphaThreshold_; + int transparentIndex_; + bool hasMultiLevelAlpha_ = false; + bool hasAlpha_ = false; + bool isGrayscale_ = false; +}; + +class OctreeColorQuantizer { + +public: + class OctreeNode { + private: + OctreeColorQuantizer* parent_ = nullptr; + + unsigned sumRed_ = 0; + unsigned sumGreen_ = 0; + unsigned sumBlue_ = 0; + int pixelCount_ = 0; + OctreeNode* children_[8] {}; + public: + OctreeNode(int level, OctreeColorQuantizer* parent); + ~OctreeNode(); + int deepPixelCount(); + + bool isEmpty() + { + return pixelCount_ == 0; + } + + bool addColor(Gdiplus::Color color, int level); + + void mergeNodes(int& leavesCount); + + Gdiplus::Color toColor() + { + assert(!isEmpty()); + return pixelCount_ == 1 + ? Gdiplus::Color((byte)sumRed_, (byte)sumGreen_, (byte)sumBlue_) + : Gdiplus::Color((byte)(sumRed_ / pixelCount_), (byte)(sumGreen_ / pixelCount_), (byte)(sumBlue_ / pixelCount_)); + } + + void populatePalette(Gdiplus::Color* result, int& palIndex, int& remainingColors); + }; + + private: + int maxColors_ = 0; + int size_ = 0; + int levelCount_ = 0; + bool hasTransparency_ = false; + std::vector<std::vector<OctreeNode*>> levels_; + std::unique_ptr<OctreeNode> root_; + int leavesCount_ = 0; + + int colorCount() + { + return leavesCount_ + (hasTransparency_ ? 1 : 0); + } + + public: + explicit OctreeColorQuantizer(int requestedColors, BYTE bitLevel, Gdiplus::BitmapData* source); + + void addColor(Gdiplus::Color color) { + if (color.GetA() == 0) { + hasTransparency_ = true; + } else if (root_->addColor(color, 0)) { + leavesCount_++; + } + } + + unique_c_ptr<Gdiplus::ColorPalette> generatePalette(); + + void reduceTree(); +}; diff --git a/Source/Core/Images/Utils.cpp b/Source/Core/Images/Utils.cpp index 4f5adad1e..ebd429cd0 100644 --- a/Source/Core/Images/Utils.cpp +++ b/Source/Core/Images/Utils.cpp @@ -34,7 +34,7 @@ #include "3rdpart/GdiplusH.h" #include "Core/Logging.h" #include "Func/WinUtils.h" -#include "3rdpart/QColorQuantizer.h" +#include "Core/Images/ColorQuantizer.h" #include "Core/Utils/StringUtils.h" #include "Core/Utils/CoreUtils.h" #include "Func/IuCommonFunctions.h" @@ -651,8 +651,8 @@ bool SaveImageToFile(Gdiplus::Bitmap* img, const CString& fileName, IStream* str eps.Parameter[0].NumberOfValues = 1; eps.Parameter[0].Value = &Quality; } else if (Format == sifGIF) { // GIF - QColorQuantizer quantizer; - quantizedImage.reset(quantizer.GetQuantized(img, QColorQuantizer::Octree, (Quality < 50) ? 16 : 256)); + ColorQuantizer quantizer; + quantizedImage = quantizer.getQuantized(img, (Quality < 50) ? 16 : 256); if (quantizedImage) { img = quantizedImage.get(); } diff --git a/Source/Gui/CMakeLists.txt b/Source/Gui/CMakeLists.txt index ee067a3df..2df3d437d 100644 --- a/Source/Gui/CMakeLists.txt +++ b/Source/Gui/CMakeLists.txt @@ -279,7 +279,6 @@ set(HEADER_LIST Helpers/LangHelper.h ../3rdpart/ColorButton.h ../3rdpart/MemberFunctionCallback.h - ../3rdpart/QColorQuantizer.h ../3rdpart/Registry.h ../3rdpart/Unzipper.h ../3rdpart/vkCodes.h diff --git a/Source/Tests/CMakeLists.txt b/Source/Tests/CMakeLists.txt index b68dc5203..26c5f0571 100644 --- a/Source/Tests/CMakeLists.txt +++ b/Source/Tests/CMakeLists.txt @@ -32,7 +32,6 @@ if(WIN32) ../Core/Images/Thumbnail.cpp ../Core/3rdpart/parser.cpp ../Func/MyUtils.cpp - ../3rdpart/QColorQuantizer.cpp ../Core/FileDownloader.cpp ../Core/FileDownloaderTest.cpp ../Video/Tests/VideoGrabberTest.cpp