mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
736 lines
27 KiB
C++
736 lines
27 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE framework.
|
|
Copyright (c) Raw Material Software Limited
|
|
|
|
JUCE is an open source framework subject to commercial or open source
|
|
licensing.
|
|
|
|
By downloading, installing, or using the JUCE framework, or combining the
|
|
JUCE framework with any other source code, object code, content or any other
|
|
copyrightable work, you agree to the terms of the JUCE End User Licence
|
|
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
|
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
|
do not agree to the terms of these agreements, we will not license the JUCE
|
|
framework to you, and you must discontinue the installation or download
|
|
process and cease use of the JUCE framework.
|
|
|
|
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
|
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
|
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
|
|
|
Or:
|
|
|
|
You may also use this code under the terms of the AGPLv3:
|
|
https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
|
|
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
|
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
|
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
namespace juce
|
|
{
|
|
|
|
class NativeReadOnlyDataReleaser : public Image::BitmapData::BitmapDataReleaser
|
|
{
|
|
public:
|
|
NativeReadOnlyDataReleaser (Image::PixelFormat pixelFormat,
|
|
int lineStride,
|
|
int w,
|
|
int h,
|
|
ComSmartPtr<ID2D1DeviceContext1> deviceContextIn,
|
|
ComSmartPtr<ID2D1Bitmap1> sourceBitmap,
|
|
Point<int> offset)
|
|
: bitmap (Direct2DBitmap::createBitmap (deviceContextIn,
|
|
pixelFormat,
|
|
{ (UINT32) w, (UINT32) h },
|
|
lineStride,
|
|
D2D1_BITMAP_OPTIONS_CPU_READ | D2D1_BITMAP_OPTIONS_CANNOT_DRAW))
|
|
{
|
|
const D2D1_POINT_2U destPoint { 0, 0 };
|
|
const Rectangle fullRect { w, h };
|
|
const auto sourceRect = D2DUtilities::toRECT_U (fullRect.getIntersection (fullRect.withPosition (offset)));
|
|
|
|
if (auto hr = bitmap->CopyFromBitmap (&destPoint, sourceBitmap, &sourceRect); FAILED (hr))
|
|
return;
|
|
|
|
D2D1_MAPPED_RECT mappedRect{};
|
|
bitmap->Map (D2D1_MAP_OPTIONS_READ, &mappedRect);
|
|
data = mappedRect.bits;
|
|
pitch = mappedRect.pitch;
|
|
}
|
|
|
|
~NativeReadOnlyDataReleaser() override
|
|
{
|
|
bitmap->Unmap();
|
|
}
|
|
|
|
auto getData() const
|
|
{
|
|
return data;
|
|
}
|
|
|
|
auto getPitch() const
|
|
{
|
|
return pitch;
|
|
}
|
|
|
|
private:
|
|
ComSmartPtr<ID2D1Bitmap1> bitmap;
|
|
BYTE* data = nullptr;
|
|
UINT32 pitch = 0;
|
|
};
|
|
|
|
class SoftwareDataReleaser : public Image::BitmapData::BitmapDataReleaser
|
|
{
|
|
public:
|
|
SoftwareDataReleaser (std::unique_ptr<Image::BitmapData::BitmapDataReleaser> r,
|
|
Image backupIn,
|
|
ComSmartPtr<ID2D1Bitmap1> nativeBitmapIn,
|
|
Image::BitmapData::ReadWriteMode modeIn,
|
|
D2D1_RECT_U targetRectIn)
|
|
: oldReleaser (std::move (r)),
|
|
backup (std::move (backupIn)),
|
|
nativeBitmap (nativeBitmapIn),
|
|
targetRect (targetRectIn),
|
|
mode (modeIn)
|
|
{
|
|
}
|
|
|
|
static void flushImage (Image softwareImage, ComSmartPtr<ID2D1Bitmap1> native, D2D1_RECT_U target)
|
|
{
|
|
if (softwareImage.getFormat() == Image::PixelFormat::RGB)
|
|
softwareImage = softwareImage.convertedToFormat (Image::PixelFormat::ARGB);
|
|
|
|
const Image::BitmapData bitmapData { softwareImage,
|
|
(int) target.left,
|
|
(int) target.top,
|
|
(int) (target.right - target.left),
|
|
(int) (target.bottom - target.top),
|
|
Image::BitmapData::readOnly };
|
|
const auto hr = native->CopyFromMemory (&target, bitmapData.data, (UINT32) bitmapData.lineStride);
|
|
jassertquiet (SUCCEEDED (hr));
|
|
}
|
|
|
|
~SoftwareDataReleaser() override
|
|
{
|
|
// Ensure that writes to the backup bitmap have been flushed before reading from it
|
|
oldReleaser = nullptr;
|
|
|
|
if (mode != Image::BitmapData::ReadWriteMode::readOnly)
|
|
flushImage (backup, nativeBitmap, targetRect);
|
|
}
|
|
|
|
private:
|
|
std::unique_ptr<Image::BitmapData::BitmapDataReleaser> oldReleaser;
|
|
Image backup;
|
|
ComSmartPtr<ID2D1Bitmap1> nativeBitmap;
|
|
D2D1_RECT_U targetRect{};
|
|
Image::BitmapData::ReadWriteMode mode{};
|
|
};
|
|
|
|
ComSmartPtr<ID2D1Bitmap1> Direct2DPixelData::createAdapterBitmap() const
|
|
{
|
|
auto bitmap = Direct2DBitmap::createBitmap (context,
|
|
pixelFormat,
|
|
{ (UINT32) width, (UINT32) height },
|
|
getLineStride(),
|
|
D2D1_BITMAP_OPTIONS_TARGET);
|
|
|
|
// The bitmap may be slightly too large due
|
|
// to DPI scaling, so fill it with transparent black
|
|
if (bitmap == nullptr || ! clearImage)
|
|
return bitmap;
|
|
|
|
context->SetTarget (bitmap);
|
|
context->BeginDraw();
|
|
context->Clear();
|
|
context->EndDraw();
|
|
context->SetTarget (nullptr);
|
|
|
|
return bitmap;
|
|
}
|
|
|
|
void Direct2DPixelData::createDeviceResources()
|
|
{
|
|
if (adapter == nullptr)
|
|
adapter = directX->adapters.getDefaultAdapter();
|
|
|
|
if (context == nullptr)
|
|
context = Direct2DDeviceContext::createContext (adapter);
|
|
|
|
if (nativeBitmap == nullptr)
|
|
{
|
|
nativeBitmap = createAdapterBitmap();
|
|
|
|
if (backup.isValid())
|
|
SoftwareDataReleaser::flushImage (backup, nativeBitmap, { 0, 0, (UINT32) width, (UINT32) height });
|
|
}
|
|
}
|
|
|
|
void Direct2DPixelData::initBitmapDataReadOnly (Image::BitmapData& bitmap, int x, int y)
|
|
{
|
|
const auto pixelStride = getPixelStride();
|
|
const auto lineStride = getLineStride();
|
|
|
|
const auto offset = (size_t) x * (size_t) pixelStride + (size_t) y * (size_t) lineStride;
|
|
bitmap.pixelFormat = pixelFormat;
|
|
bitmap.pixelStride = pixelStride;
|
|
bitmap.lineStride = lineStride;
|
|
bitmap.size = (size_t) (height * lineStride) - offset;
|
|
|
|
JUCE_TRACE_LOG_D2D_IMAGE_MAP_DATA;
|
|
|
|
auto releaser = std::make_unique<NativeReadOnlyDataReleaser> (pixelFormat,
|
|
lineStride,
|
|
width,
|
|
height,
|
|
context,
|
|
getAdapterD2D1Bitmap(),
|
|
Point { x, y });
|
|
bitmap.data = releaser->getData();
|
|
bitmap.lineStride = (int) releaser->getPitch();
|
|
bitmap.dataReleaser = std::move (releaser);
|
|
}
|
|
|
|
auto Direct2DPixelData::make (Image::PixelFormat formatToUse,
|
|
int widthIn,
|
|
int heightIn,
|
|
bool clearImageIn,
|
|
DxgiAdapter::Ptr adapterIn) -> Ptr
|
|
{
|
|
return new Direct2DPixelData (formatToUse, widthIn, heightIn, clearImageIn, adapterIn);
|
|
}
|
|
|
|
auto Direct2DPixelData::fromDirect2DBitmap (ComSmartPtr<ID2D1Bitmap1> bitmap) -> Ptr
|
|
{
|
|
const auto size = bitmap->GetPixelSize();
|
|
return new Direct2DPixelData { Image::ARGB, (int) size.width, (int) size.height, false, nullptr };
|
|
}
|
|
|
|
Direct2DPixelData::Direct2DPixelData (Image::PixelFormat f, int widthIn, int heightIn, bool clear, DxgiAdapter::Ptr adapterIn)
|
|
: ImagePixelData (f, widthIn, heightIn),
|
|
clearImage (clear),
|
|
adapter (adapterIn != nullptr ? adapterIn : directX->adapters.getDefaultAdapter())
|
|
{
|
|
directX->adapters.addListener (*this);
|
|
}
|
|
|
|
Direct2DPixelData::~Direct2DPixelData()
|
|
{
|
|
directX->adapters.removeListener (*this);
|
|
}
|
|
|
|
std::unique_ptr<LowLevelGraphicsContext> Direct2DPixelData::createLowLevelContext()
|
|
{
|
|
sendDataChangeMessage();
|
|
|
|
return std::make_unique<Direct2DImageContext> (this);
|
|
}
|
|
|
|
void Direct2DPixelData::initialiseBitmapData (Image::BitmapData& bitmap, int x, int y, Image::BitmapData::ReadWriteMode mode)
|
|
{
|
|
JUCE_TRACE_LOG_D2D_IMAGE_MAP_DATA;
|
|
|
|
// The native format matches the JUCE format, and there's no need to write from CPU->GPU, so
|
|
// map the GPU memory as read-only and return that.
|
|
if (mode == Image::BitmapData::ReadWriteMode::readOnly && pixelFormat != Image::PixelFormat::RGB)
|
|
{
|
|
initBitmapDataReadOnly (bitmap, x, y);
|
|
return;
|
|
}
|
|
|
|
// The native format does not match the JUCE format, or the user wants to read the current state of the image.
|
|
// If the user wants to read the image, then we'll need to copy it to CPU memory.
|
|
if (mode != Image::BitmapData::ReadWriteMode::writeOnly)
|
|
{
|
|
// Store the previous width and height, and set up the BitmapData to cover the entire image area
|
|
const auto oldW = std::exchange (bitmap.width, width);
|
|
const auto oldH = std::exchange (bitmap.height, height);
|
|
|
|
// Map the image as read-only.
|
|
initBitmapDataReadOnly (bitmap, 0, 0);
|
|
// Copy the mapped image to CPU memory in the correct format.
|
|
backup = BitmapDataDetail::convert (bitmap, SoftwareImageType{});
|
|
// Unmap the image (important, the BitmapData is reused later on).
|
|
bitmap.dataReleaser = {};
|
|
|
|
// Reset the initial width and height
|
|
bitmap.width = oldW;
|
|
bitmap.height = oldH;
|
|
}
|
|
|
|
// If the user doesn't want to read from the image, then we may need to create a blank image that they can write to.
|
|
if (! backup.isValid())
|
|
backup = Image { SoftwareImageType{}.create (pixelFormat, width, height, clearImage) };
|
|
|
|
// Redirect the BitmapData to our backup software image.
|
|
backup.getPixelData()->initialiseBitmapData (bitmap, x, y, mode);
|
|
|
|
// When this dataReleaser is destroyed, then if the mode is not read-only, image data will be copied
|
|
// from the software image to GPU memory.
|
|
bitmap.dataReleaser = std::make_unique<SoftwareDataReleaser> (std::move (bitmap.dataReleaser),
|
|
backup,
|
|
getAdapterD2D1Bitmap(),
|
|
mode,
|
|
D2D1_RECT_U { (UINT32) x, (UINT32) y, (UINT32) width, (UINT32) height });
|
|
}
|
|
|
|
void Direct2DPixelData::flushToSoftwareBackup()
|
|
{
|
|
backup = SoftwareImageType{}.convert (Image { this });
|
|
}
|
|
|
|
ImagePixelData::Ptr Direct2DPixelData::clone()
|
|
{
|
|
auto cloned = make (pixelFormat, width, height, false, nullptr);
|
|
|
|
if (cloned == nullptr)
|
|
return {};
|
|
|
|
cloned->backup = backup.createCopy();
|
|
|
|
const D2D1_POINT_2U destinationPoint { 0, 0 };
|
|
const auto sourceRectU = D2DUtilities::toRECT_U (Rectangle { width, height });
|
|
const auto sourceD2D1Bitmap = getAdapterD2D1Bitmap();
|
|
const auto destinationD2D1Bitmap = cloned->getAdapterD2D1Bitmap();
|
|
|
|
if (sourceD2D1Bitmap == nullptr || destinationD2D1Bitmap == nullptr)
|
|
return {};
|
|
|
|
if (const auto hr = destinationD2D1Bitmap->CopyFromBitmap (&destinationPoint, sourceD2D1Bitmap, &sourceRectU); FAILED (hr))
|
|
{
|
|
jassertfalse;
|
|
return {};
|
|
}
|
|
|
|
return cloned;
|
|
}
|
|
|
|
void Direct2DPixelData::applyGaussianBlurEffect (float radius, Image& result)
|
|
{
|
|
// The result must be a separate image!
|
|
jassert (result.getPixelData() != this);
|
|
|
|
if (context == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
ComSmartPtr<ID2D1Effect> effect;
|
|
if (const auto hr = context->CreateEffect (CLSID_D2D1GaussianBlur, effect.resetAndGetPointerAddress());
|
|
FAILED (hr) || effect == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
effect->SetInput (0, getAdapterD2D1Bitmap());
|
|
effect->SetValue (D2D1_GAUSSIANBLUR_PROP_STANDARD_DEVIATION, radius / 3.0f);
|
|
|
|
const auto tie = [] (const auto& x) { return std::tuple (x.pixelFormat, x.width, x.height); };
|
|
const auto originalPixelData = dynamic_cast<Direct2DPixelData*> (result.getPixelData());
|
|
|
|
if (originalPixelData == nullptr || tie (*this) != tie (*originalPixelData))
|
|
result = Image { make (pixelFormat, width, height, false, adapter) };
|
|
|
|
const auto outputPixelData = dynamic_cast<Direct2DPixelData*> (result.getPixelData());
|
|
|
|
if (outputPixelData == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
outputPixelData->createDeviceResources();
|
|
auto outputDataContext = outputPixelData->context;
|
|
|
|
if (outputDataContext == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
outputDataContext->SetTarget (outputPixelData->getAdapterD2D1Bitmap());
|
|
outputDataContext->BeginDraw();
|
|
outputDataContext->Clear();
|
|
outputDataContext->DrawImage (effect);
|
|
outputDataContext->EndDraw();
|
|
outputDataContext->SetTarget (nullptr);
|
|
}
|
|
|
|
void Direct2DPixelData::applySingleChannelBoxBlurEffect (int radius, Image& result)
|
|
{
|
|
// The result must be a separate image!
|
|
jassert (result.getPixelData() != this);
|
|
|
|
if (context == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
constexpr FLOAT kernel[] { 1.0f / 9.0f, 2.0f / 9.0f, 3.0f / 9.0f, 2.0f / 9.0f, 1.0f / 9.0f };
|
|
|
|
ComSmartPtr<ID2D1Effect> begin, end;
|
|
|
|
for (auto horizontal : { false, true })
|
|
{
|
|
for (auto i = 0; i < radius; ++i)
|
|
{
|
|
ComSmartPtr<ID2D1Effect> effect;
|
|
if (const auto hr = context->CreateEffect (CLSID_D2D1ConvolveMatrix, effect.resetAndGetPointerAddress());
|
|
FAILED (hr) || effect == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_SIZE_X, (UINT32) (horizontal ? std::size (kernel) : 1));
|
|
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_SIZE_Y, (UINT32) (horizontal ? 1 : std::size (kernel)));
|
|
effect->SetValue (D2D1_CONVOLVEMATRIX_PROP_KERNEL_MATRIX, kernel);
|
|
|
|
if (begin == nullptr)
|
|
{
|
|
begin = effect;
|
|
end = effect;
|
|
}
|
|
else
|
|
{
|
|
effect->SetInputEffect (0, end);
|
|
end = effect;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (begin == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
begin->SetInput (0, getAdapterD2D1Bitmap());
|
|
|
|
const auto originalPixelData = dynamic_cast<Direct2DPixelData*> (result.getPixelData());
|
|
|
|
if (originalPixelData == nullptr || std::tuple (Image::SingleChannel, width, height) != std::tuple (originalPixelData->pixelFormat, originalPixelData->width, originalPixelData->height))
|
|
result = Image { make (Image::SingleChannel, width, height, false, adapter) };
|
|
|
|
const auto outputPixelData = dynamic_cast<Direct2DPixelData*> (result.getPixelData());
|
|
|
|
if (outputPixelData == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
outputPixelData->createDeviceResources();
|
|
auto outputDataContext = outputPixelData->context;
|
|
|
|
if (outputDataContext == nullptr)
|
|
{
|
|
result = {};
|
|
return;
|
|
}
|
|
|
|
outputDataContext->SetTarget (outputPixelData->getAdapterD2D1Bitmap());
|
|
outputDataContext->BeginDraw();
|
|
outputDataContext->Clear();
|
|
outputDataContext->DrawImage (end);
|
|
outputDataContext->EndDraw();
|
|
outputDataContext->SetTarget (nullptr);
|
|
}
|
|
|
|
std::unique_ptr<ImageType> Direct2DPixelData::createType() const
|
|
{
|
|
return std::make_unique<NativeImageType>();
|
|
}
|
|
|
|
void Direct2DPixelData::adapterCreated (DxgiAdapter::Ptr)
|
|
{
|
|
}
|
|
|
|
void Direct2DPixelData::adapterRemoved (DxgiAdapter::Ptr)
|
|
{
|
|
adapter = nullptr;
|
|
context = nullptr;
|
|
nativeBitmap = nullptr;
|
|
}
|
|
|
|
ComSmartPtr<ID2D1Bitmap1> Direct2DPixelData::getAdapterD2D1Bitmap()
|
|
{
|
|
createDeviceResources();
|
|
return nativeBitmap;
|
|
}
|
|
|
|
//==============================================================================
|
|
ImagePixelData::Ptr NativeImageType::create (Image::PixelFormat format, int width, int height, bool clearImage) const
|
|
{
|
|
SharedResourcePointer<DirectX> directX;
|
|
|
|
if (directX->adapters.getFactory() == nullptr)
|
|
{
|
|
// Make sure the DXGI factory exists
|
|
//
|
|
// The caller may be trying to create an Image from a static variable; if this is a DLL, then this is
|
|
// probably called from DllMain. You can't create a DXGI factory from DllMain, so fall back to a
|
|
// software image.
|
|
return new SoftwarePixelData { format, width, height, clearImage };
|
|
}
|
|
|
|
return Direct2DPixelData::make (format, width, height, clearImage, nullptr);
|
|
}
|
|
|
|
//==============================================================================
|
|
//==============================================================================
|
|
#if JUCE_UNIT_TESTS
|
|
|
|
class Direct2DImageUnitTest final : public UnitTest
|
|
{
|
|
public:
|
|
Direct2DImageUnitTest()
|
|
: UnitTest ("Direct2DImageUnitTest", UnitTestCategories::graphics)
|
|
{
|
|
compareFunctions[{ Image::RGB, Image::RGB }] = [] (uint8* rgb1, uint8* rgb2)
|
|
{
|
|
return rgb1[0] == rgb2[0] && rgb1[1] == rgb2[1] && rgb1[2] == rgb2[2];
|
|
};
|
|
|
|
compareFunctions[{ Image::RGB, Image::ARGB }] = [] (uint8* rgb, uint8* argb)
|
|
{
|
|
// Compare bytes directly to avoid alpha premultiply issues
|
|
return rgb[0] == argb[0] && // blue
|
|
rgb[1] == argb[1] && // green
|
|
rgb[2] == argb[2]; // red
|
|
};
|
|
|
|
compareFunctions[{ Image::RGB, Image::SingleChannel }] = [] (uint8*, uint8* singleChannel)
|
|
{
|
|
return *singleChannel == 0xff;
|
|
};
|
|
|
|
compareFunctions[{ Image::ARGB, Image::RGB }] = [] (uint8* argb, uint8* rgb)
|
|
{
|
|
// Compare bytes directly to avoid alpha premultiply issues
|
|
return argb[0] == rgb[0] && argb[1] == rgb[1] && argb[2] == rgb[2];
|
|
};
|
|
|
|
compareFunctions[{ Image::ARGB, Image::ARGB }] = [] (uint8* argb1, uint8* argb2)
|
|
{
|
|
return *reinterpret_cast<uint32*> (argb1) == *reinterpret_cast<uint32*> (argb2);
|
|
};
|
|
|
|
compareFunctions[{ Image::ARGB, Image::SingleChannel }] = [] (uint8* argb, uint8* singleChannel)
|
|
{
|
|
return argb[3] = *singleChannel;
|
|
};
|
|
|
|
compareFunctions[{ Image::SingleChannel, Image::RGB }] = [] (uint8* singleChannel, uint8* rgb)
|
|
{
|
|
auto alpha = *singleChannel;
|
|
return rgb[0] == alpha && rgb[1] == alpha && rgb[2] == alpha;
|
|
};
|
|
|
|
compareFunctions[{ Image::SingleChannel, Image::ARGB }] = [] (uint8* singleChannel, uint8* argb)
|
|
{
|
|
return *singleChannel = argb[3];
|
|
};
|
|
|
|
compareFunctions[{ Image::SingleChannel, Image::SingleChannel }] = [] (uint8* singleChannel1, uint8* singleChannel2)
|
|
{
|
|
return *singleChannel1 == *singleChannel2;
|
|
};
|
|
}
|
|
|
|
void runTest() override
|
|
{
|
|
beginTest ("Direct2DImageUnitTest");
|
|
|
|
random = getRandom();
|
|
|
|
for (auto format : formats)
|
|
compareSameFormat (format);
|
|
|
|
testFormatConversion();
|
|
}
|
|
|
|
Rectangle<int> randomRectangleWithin (Rectangle<int> container) noexcept
|
|
{
|
|
auto x = random.nextInt (container.getWidth() - 2);
|
|
auto y = random.nextInt (container.getHeight() - 2);
|
|
auto w = random.nextInt (container.getHeight() - x);
|
|
auto h = random.nextInt (container.getWidth() - y);
|
|
h = jmax (h, 1);
|
|
w = jmax (w, 1);
|
|
return Rectangle<int> { x, y, w, h };
|
|
}
|
|
|
|
void compareSameFormat (Image::PixelFormat format)
|
|
{
|
|
auto softwareImage = Image { SoftwareImageType{}.create (format, 100, 100, true) };
|
|
{
|
|
Graphics g { softwareImage };
|
|
g.fillCheckerBoard (softwareImage.getBounds().toFloat(), 21.0f, 21.0f, makeRandomColor(), makeRandomColor());
|
|
}
|
|
|
|
auto direct2DImage = NativeImageType{}.convert (softwareImage);
|
|
|
|
compareImages (softwareImage, direct2DImage, compareFunctions[{ softwareImage.getFormat(), direct2DImage.getFormat() }]);
|
|
checkReadWriteModes (softwareImage);
|
|
checkReadWriteModes (direct2DImage);
|
|
}
|
|
|
|
void compareImages (Image& image1, Image& image2, std::function<bool (uint8*, uint8*)> compareBytes)
|
|
{
|
|
{
|
|
// BitmapData width & height should match
|
|
Rectangle<int> area = randomRectangleWithin (image1.getBounds());
|
|
Image::BitmapData data1 { image1, area.getX(), area.getY(), area.getWidth(), area.getHeight(), Image::BitmapData::ReadWriteMode::readOnly };
|
|
Image::BitmapData data2 { image2, area.getX(), area.getY(), area.getWidth(), area.getHeight(), Image::BitmapData::ReadWriteMode::readOnly };
|
|
|
|
expect (data1.width == data2.width);
|
|
expect (data1.height == data2.height);
|
|
}
|
|
|
|
{
|
|
// Bitmap data should match after ImageType::convert
|
|
Image::BitmapData data1 { image1, Image::BitmapData::ReadWriteMode::readOnly };
|
|
Image::BitmapData data2 { image2, Image::BitmapData::ReadWriteMode::readOnly };
|
|
|
|
for (int y = 0; y < data1.height; ++y)
|
|
{
|
|
auto line1 = data1.getLinePointer (y);
|
|
auto line2 = data2.getLinePointer (y);
|
|
|
|
for (int x = 0; x < data1.width; ++x)
|
|
{
|
|
expect (compareBytes (line1, line2), "Failed comparing format " + String { image1.getFormat() } + " to " + String { image2.getFormat() });
|
|
|
|
line1 += data1.pixelStride;
|
|
line2 += data2.pixelStride;
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
// Subsection data should match
|
|
// Should be able to have two different BitmapData objects simultaneously for the same source image
|
|
Rectangle<int> area1 = randomRectangleWithin (image1.getBounds());
|
|
Rectangle<int> area2 = randomRectangleWithin (image1.getBounds());
|
|
Image::BitmapData data1 { image1, Image::BitmapData::ReadWriteMode::readOnly };
|
|
Image::BitmapData data2a { image2, area1.getX(), area1.getY(), area1.getWidth(), area1.getHeight(), Image::BitmapData::ReadWriteMode::readOnly };
|
|
Image::BitmapData data2b { image2, area2.getX(), area2.getY(), area2.getWidth(), area2.getHeight(), Image::BitmapData::ReadWriteMode::readOnly };
|
|
|
|
auto compareSubsection = [&] (Image::BitmapData& subsection1, Image::BitmapData& subsection2, Rectangle<int> area)
|
|
{
|
|
for (int y = 0; y < area.getHeight(); ++y)
|
|
{
|
|
auto line1 = subsection1.getLinePointer (y + area.getY());
|
|
auto line2 = subsection2.getLinePointer (y);
|
|
|
|
for (int x = 0; x < area.getWidth(); ++x)
|
|
{
|
|
expect (compareBytes (line1 + (x + area.getX()) * subsection1.pixelStride, line2 + x * subsection2.pixelStride));
|
|
}
|
|
}
|
|
};
|
|
|
|
compareSubsection (data1, data2a, area1);
|
|
compareSubsection (data1, data2b, area2);
|
|
}
|
|
}
|
|
|
|
void checkReadWriteModes (Image& image)
|
|
{
|
|
// Check read and write modes
|
|
int x = random.nextInt (image.getWidth());
|
|
auto writeColor = makeRandomColor().withAlpha (1.0f);
|
|
auto expectedColor = writeColor;
|
|
switch (image.getFormat())
|
|
{
|
|
case Image::SingleChannel:
|
|
{
|
|
auto alpha = writeColor.getAlpha();
|
|
expectedColor = Colour { alpha, alpha, alpha, alpha };
|
|
break;
|
|
}
|
|
|
|
case Image::RGB:
|
|
case Image::ARGB:
|
|
break;
|
|
|
|
case Image::UnknownFormat:
|
|
default:
|
|
jassertfalse;
|
|
break;
|
|
}
|
|
|
|
{
|
|
Image::BitmapData data { image, Image::BitmapData::ReadWriteMode::writeOnly };
|
|
|
|
for (int y = 0; y < data.height; ++y)
|
|
data.setPixelColour (x, y, writeColor);
|
|
}
|
|
|
|
{
|
|
Image::BitmapData data { image, Image::BitmapData::ReadWriteMode::readOnly };
|
|
|
|
for (int y = 0; y < data.height; ++y)
|
|
{
|
|
auto color = data.getPixelColour (x, y);
|
|
expect (color == expectedColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
void testFormatConversion()
|
|
{
|
|
for (auto sourceFormat : formats)
|
|
{
|
|
for (auto destFormat : formats)
|
|
{
|
|
auto softwareStartImage = Image { SoftwareImageType {}.create (sourceFormat, 100, 100, true) };
|
|
{
|
|
Graphics g { softwareStartImage };
|
|
g.fillCheckerBoard (softwareStartImage.getBounds().toFloat(), 21.0f, 21.0f, makeRandomColor(), makeRandomColor());
|
|
}
|
|
auto convertedSoftwareImage = softwareStartImage.convertedToFormat (destFormat);
|
|
|
|
compareImages (softwareStartImage, convertedSoftwareImage, compareFunctions[{ sourceFormat, destFormat }]);
|
|
|
|
auto direct2DImage = NativeImageType {}.convert (softwareStartImage);
|
|
|
|
compareImages (softwareStartImage, direct2DImage, compareFunctions[{ sourceFormat, sourceFormat }]);
|
|
|
|
auto convertedDirect2DImage = direct2DImage.convertedToFormat (destFormat);
|
|
|
|
compareImages (softwareStartImage, convertedDirect2DImage, compareFunctions[{ sourceFormat, destFormat }]);
|
|
}
|
|
}
|
|
}
|
|
|
|
Colour makeRandomColor()
|
|
{
|
|
uint8 red = (uint8) random.nextInt (255);
|
|
uint8 green = (uint8) random.nextInt (255);
|
|
uint8 blue = (uint8) random.nextInt (255);
|
|
uint8 alpha = (uint8) random.nextInt (255);
|
|
return Colour { red, green, blue, alpha };
|
|
}
|
|
|
|
|
|
Random random;
|
|
std::array<Image::PixelFormat, 3> const formats { Image::RGB, Image::ARGB, Image::SingleChannel };
|
|
std::map<std::pair<Image::PixelFormat, Image::PixelFormat>, std::function<bool (uint8*, uint8*)>> compareFunctions;
|
|
};
|
|
|
|
static Direct2DImageUnitTest direct2DImageUnitTest;
|
|
|
|
#endif
|
|
|
|
} // namespace juce
|