/* * Image.cpp * --------- * Purpose: Bitmap and Vector image file handling using GDI+. * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "MPTrackUtil.h" #include "Image.h" #include "../common/FileReader.h" #include "../common/ComponentManager.h" // GDI+ #include #define max(a, b) (((a) > (b)) ? (a) : (b)) #define min(a, b) (((a) < (b)) ? (a) : (b)) #if MPT_COMPILER_MSVC #pragma warning(push) #pragma warning(disable : 4458) // declaration of 'x' hides class member #endif #include #if MPT_COMPILER_MSVC #pragma warning(pop) #endif #undef min #undef max OPENMPT_NAMESPACE_BEGIN GdiplusRAII::GdiplusRAII() { Gdiplus::GdiplusStartupInput gdiplusStartupInput; Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); } GdiplusRAII::~GdiplusRAII() { Gdiplus::GdiplusShutdown(gdiplusToken); gdiplusToken = 0; } RawGDIDIB::RawGDIDIB(uint32 width, uint32 height) : width(width) , height(height) , pixels(width * height) { MPT_ASSERT(width > 0); MPT_ASSERT(height > 0); } namespace GDIP { static CComPtr GetStream(mpt::const_byte_span data) { CComPtr stream; #if (_WIN32_WINNT >= _WIN32_WINNT_VISTA) stream.Attach(SHCreateMemStream(mpt::byte_cast(data.data()), mpt::saturate_cast(data.size()))); #else HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, data.size()); if(hGlobal == NULL) { throw bad_image(); } void * mem = GlobalLock(hGlobal); if(!mem) { hGlobal = GlobalFree(hGlobal); throw bad_image(); } std::memcpy(mem, data.data(), data.size()); GlobalUnlock(hGlobal); if(CreateStreamOnHGlobal(hGlobal, TRUE, &stream) != S_OK) { hGlobal = GlobalFree(hGlobal); throw bad_image(); } hGlobal = NULL; #endif if(!stream) { throw bad_image(); } return stream; } std::unique_ptr LoadPixelImage(mpt::const_byte_span file) { CComPtr stream = GetStream(file); std::unique_ptr result = std::make_unique(stream, FALSE); if(result->GetLastStatus() != Gdiplus::Ok) { throw bad_image(); } if(result->GetWidth() == 0 || result->GetHeight() == 0) { throw bad_image(); } return result; } std::unique_ptr LoadPixelImage(FileReader file) { FileReader::PinnedView view = file.GetPinnedView(); return LoadPixelImage(view.span()); } std::unique_ptr LoadVectorImage(mpt::const_byte_span file) { CComPtr stream = GetStream(file); std::unique_ptr result = std::make_unique(stream); if(result->GetLastStatus() != Gdiplus::Ok) { throw bad_image(); } if(result->GetWidth() == 0 || result->GetHeight() == 0) { throw bad_image(); } return result; } std::unique_ptr LoadVectorImage(FileReader file) { FileReader::PinnedView view = file.GetPinnedView(); return LoadVectorImage(view.span()); } static std::unique_ptr DoResize(Gdiplus::Image &src, double scaling, int spriteWidth, int spriteHeight) { const int width = src.GetWidth(), height = src.GetHeight(); int newWidth = 0, newHeight = 0, newSpriteWidth = 0, newSpriteHeight = 0; if(spriteWidth <= 0 || spriteHeight <= 0) { newWidth = mpt::saturate_round(width * scaling); newHeight = mpt::saturate_round(height * scaling); } else { // Sprite mode: Source images consists of several sprites / icons that should be scaled individually newSpriteWidth = mpt::saturate_round(spriteWidth * scaling); newSpriteHeight = mpt::saturate_round(spriteHeight * scaling); newWidth = width * newSpriteWidth / spriteWidth; newHeight = height * newSpriteHeight / spriteHeight; } std::unique_ptr resizedImage = std::make_unique(newWidth, newHeight, PixelFormat32bppARGB); std::unique_ptr resizedGraphics(Gdiplus::Graphics::FromImage(resizedImage.get())); if(scaling >= 1.5) { // Prefer crisp look on real high-DPI devices resizedGraphics->SetInterpolationMode(Gdiplus::InterpolationModeNearestNeighbor); resizedGraphics->SetPixelOffsetMode(Gdiplus::PixelOffsetModeHalf); resizedGraphics->SetSmoothingMode(Gdiplus::SmoothingModeNone); } else { resizedGraphics->SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic); resizedGraphics->SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality); resizedGraphics->SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); } if(spriteWidth <= 0 || spriteHeight <= 0) { resizedGraphics->DrawImage(&src, 0, 0, newWidth, newHeight); } else { // Draw each source sprite individually into separate image to avoid neighbouring source sprites bleeding in std::unique_ptr spriteImage = std::make_unique(spriteWidth, spriteHeight, PixelFormat32bppARGB); std::unique_ptr spriteGraphics(Gdiplus::Graphics::FromImage(spriteImage.get())); for(int srcY = 0, destY = 0; srcY < height; srcY += spriteHeight, destY += newSpriteHeight) { for(int srcX = 0, destX = 0; srcX < width; srcX += spriteWidth, destX += newSpriteWidth) { spriteGraphics->Clear({0, 0, 0, 0}); spriteGraphics->DrawImage(&src, Gdiplus::Rect(0, 0, spriteWidth, spriteHeight), srcX, srcY, spriteWidth, spriteHeight, Gdiplus::UnitPixel); resizedGraphics->DrawImage(spriteImage.get(), destX, destY, newSpriteWidth, newSpriteHeight); } } } return resizedImage; } std::unique_ptr ResizeImage(Gdiplus::Image &src, double scaling, int spriteWidth, int spriteHeight) { return DoResize(src, scaling, spriteWidth, spriteHeight); } std::unique_ptr ResizeImage(Gdiplus::Bitmap &src, double scaling, int spriteWidth, int spriteHeight) { return DoResize(src, scaling, spriteWidth, spriteHeight); } } // namespace GDIP std::unique_ptr ToRawGDIDIB(Gdiplus::Bitmap &bitmap) { Gdiplus::BitmapData bitmapData; Gdiplus::Rect rect{Gdiplus::Point{0, 0}, Gdiplus::Size{static_cast(bitmap.GetWidth()), static_cast(bitmap.GetHeight())}}; std::unique_ptr result = std::make_unique(bitmap.GetWidth(), bitmap.GetHeight()); if(bitmap.LockBits(&rect, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, &bitmapData) != Gdiplus::Ok) { throw bad_image(); } RawGDIDIB::Pixel *dst = result->Pixels().data(); for(uint32 y = 0; y < result->Height(); ++y) { const GDIP::Pixel *src = GDIP::GetScanline(bitmapData, y); for(uint32 x = 0; x < result->Width(); ++x) { *dst = GDIP::ToRawGDIDIB(*src); src++; dst++; } } bitmap.UnlockBits(&bitmapData); return result; } std::unique_ptr LoadPixelImage(mpt::const_byte_span file, double scaling, int spriteWidth, int spriteHeight) { auto bitmap = GDIP::LoadPixelImage(file); if(scaling != 1.0) bitmap = GDIP::ResizeImage(*bitmap, scaling, spriteWidth, spriteHeight); return ToRawGDIDIB(*bitmap); } std::unique_ptr LoadPixelImage(FileReader file, double scaling, int spriteWidth, int spriteHeight) { auto bitmap = GDIP::LoadPixelImage(file); if(scaling != 1.0) bitmap = GDIP::ResizeImage(*bitmap, scaling, spriteWidth, spriteHeight); return ToRawGDIDIB(*bitmap); } // Create a DIB for the current device from our PNG. bool CopyToCompatibleBitmap(CBitmap &dst, CDC &dc, const RawGDIDIB &src) { BITMAPINFOHEADER bi; MemsetZero(bi); bi.biSize = sizeof(BITMAPINFOHEADER); bi.biWidth = src.Width(); bi.biHeight = -static_cast(src.Height()); bi.biPlanes = 1; bi.biBitCount = 32; bi.biCompression = BI_RGB; bi.biSizeImage = src.Width() * src.Height() * 4; if(!dst.CreateCompatibleBitmap(&dc, src.Width(), src.Height())) { return false; } if(!SetDIBits(dc.GetSafeHdc(), dst, 0, src.Height(), src.Pixels().data(), reinterpret_cast(&bi), DIB_RGB_COLORS)) { return false; } return true; } bool CopyToCompatibleBitmap(CBitmap &dst, CDC &dc, Gdiplus::Image &src) { if(!dst.CreateCompatibleBitmap(&dc, src.GetWidth(), src.GetHeight())) { return false; } CDC memdc; if(!memdc.CreateCompatibleDC(&dc)) { return false; } memdc.SelectObject(dst); Gdiplus::Graphics gfx(memdc); if(gfx.DrawImage(&src, 0, 0) != Gdiplus::Ok) { return false; } return true; } bool LoadCompatibleBitmapFromPixelImage(CBitmap &dst, CDC &dc, mpt::const_byte_span file) { try { std::unique_ptr pBitmap = GDIP::LoadPixelImage(file); if(!CopyToCompatibleBitmap(dst, dc, *pBitmap)) { return false; } } catch(...) { return false; } return true; } bool LoadCompatibleBitmapFromPixelImage(CBitmap &dst, CDC &dc, FileReader file) { try { std::unique_ptr pBitmap = GDIP::LoadPixelImage(file); if(!CopyToCompatibleBitmap(dst, dc, *pBitmap)) { return false; } } catch(...) { return false; } return true; } OPENMPT_NAMESPACE_END