mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-09 23:34:20 +00:00
Previously, this code assumed that a single channel image would always have a pixel stride of 1. For image types where this assumption did not hold, such as OpenGL, this function would produce incorrect results.
1073 lines
36 KiB
C++
1073 lines
36 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
|
|
{
|
|
|
|
namespace BitmapDataDetail
|
|
{
|
|
template <auto T>
|
|
using FormatConstant = std::integral_constant<decltype (T), T>;
|
|
|
|
using ARGB = FormatConstant<Image::PixelFormat::ARGB>;
|
|
using RGB = FormatConstant<Image::PixelFormat::RGB>;
|
|
using A = FormatConstant<Image::PixelFormat::SingleChannel>;
|
|
|
|
static Colour getPixelColour (const uint8* pixel, A)
|
|
{
|
|
return Colour (*((const PixelAlpha*) pixel));
|
|
}
|
|
|
|
static Colour getPixelColour (const uint8* pixel, RGB)
|
|
{
|
|
return Colour (*((const PixelRGB*) pixel));
|
|
}
|
|
|
|
static Colour getPixelColour (const uint8* pixel, ARGB)
|
|
{
|
|
return Colour (((const PixelARGB*) pixel)->getUnpremultiplied());
|
|
}
|
|
|
|
static void setPixelColour (uint8* pixel, PixelARGB col, A)
|
|
{
|
|
((PixelAlpha*) pixel)->set (col);
|
|
}
|
|
|
|
static void setPixelColour (uint8* pixel, PixelARGB col, RGB)
|
|
{
|
|
((PixelRGB*) pixel)->set (col);
|
|
}
|
|
|
|
static void setPixelColour (uint8* pixel, PixelARGB col, ARGB)
|
|
{
|
|
((PixelARGB*) pixel)->set (col);
|
|
}
|
|
|
|
using ConverterFn = void (*) (const Image::BitmapData& src, const Image::BitmapData& dst, int w, int h);
|
|
|
|
template <typename From, typename To>
|
|
static constexpr ConverterFn makeConverterFn (From, To)
|
|
{
|
|
struct GetPixel
|
|
{
|
|
explicit GetPixel (const Image::BitmapData& bd)
|
|
: data (bd.data),
|
|
lineStride ((size_t) bd.lineStride),
|
|
pixelStride ((size_t) bd.pixelStride) {}
|
|
|
|
uint8* operator() (int x, int y) const
|
|
{
|
|
return data + (size_t) y * (size_t) lineStride + (size_t) x * (size_t) pixelStride;
|
|
}
|
|
|
|
uint8* data;
|
|
size_t lineStride, pixelStride;
|
|
};
|
|
|
|
return [] (const Image::BitmapData& src, const Image::BitmapData& dst, int w, int h)
|
|
{
|
|
const GetPixel getSrc { src }, getDst { dst };
|
|
|
|
for (int y = 0; y < h; ++y)
|
|
{
|
|
for (int x = 0; x < w; ++x)
|
|
{
|
|
const auto srcColour = getPixelColour (getSrc (x, y), From{});
|
|
setPixelColour (getDst (x, y), srcColour.getPixelARGB(), To{});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
template <typename From, typename... To>
|
|
static constexpr auto makeConverterFns (From from, To... to)
|
|
{
|
|
return std::array<ConverterFn, sizeof... (To)> { makeConverterFn (from, to)... };
|
|
}
|
|
|
|
/** This structure holds a 2D array of function pointers.
|
|
The structure is indexed by source format and destination format, where the function
|
|
at that index will convert an entire BitmapData array between those two formats.
|
|
|
|
This approach is designed to avoid branching, especially switch statements, from
|
|
the inner loop of the conversion. At time of writing, compilers cannot automatically
|
|
vectorise loops containing switch statements. Therefore, it's often faster to move
|
|
switches or table lookups outside tight loops.
|
|
|
|
This is the old, slow approach:
|
|
|
|
@code
|
|
for (int y = 0; y < dest.height; ++y)
|
|
for (int x = 0; x < dest.width; ++x)
|
|
dest.setPixelColour (x, y, src.getPixelColour (x, y));
|
|
@endcode
|
|
|
|
This is a faster way to write the same thing:
|
|
|
|
@code
|
|
if (const auto* converter = converterFnTable.getConverterFor (src.pixelFormat, dest.pixelFormat))
|
|
converter (src, dest, dest.width, dest.height);
|
|
@endcode
|
|
*/
|
|
template <typename... Formats>
|
|
class ConverterFnTable
|
|
{
|
|
public:
|
|
constexpr ConverterFn getConverterFor (Image::PixelFormat src, Image::PixelFormat dst) const
|
|
{
|
|
const auto srcIndex = toIndex (src, Formats{}...);
|
|
const auto dstIndex = toIndex (dst, Formats{}...);
|
|
|
|
if (srcIndex >= sizeof... (Formats) || dstIndex >= sizeof... (Formats))
|
|
return nullptr;
|
|
|
|
return table[srcIndex][dstIndex];
|
|
}
|
|
|
|
private:
|
|
static size_t toIndex (Image::PixelFormat)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
template <typename Head, typename... Tail>
|
|
static size_t toIndex (Image::PixelFormat src, Head head, Tail... tail)
|
|
{
|
|
return src == head() ? 0 : 1 + toIndex (src, tail...);
|
|
}
|
|
|
|
std::array<std::array<ConverterFn, sizeof... (Formats)>, sizeof... (Formats)> table
|
|
{
|
|
makeConverterFns (Formats{}, Formats{}...)...
|
|
};
|
|
};
|
|
|
|
static bool convert (const Image::BitmapData& src, Image::BitmapData& dest)
|
|
{
|
|
if (std::tuple (src.width, src.height) != std::tuple (dest.width, dest.height))
|
|
return false;
|
|
|
|
static constexpr auto converterFnTable = ConverterFnTable<RGB, ARGB, A>{};
|
|
|
|
if (src.pixelStride == dest.pixelStride && src.pixelFormat == dest.pixelFormat)
|
|
{
|
|
for (int y = 0; y < dest.height; ++y)
|
|
memcpy (dest.getLinePointer (y), src.getLinePointer (y), (size_t) dest.pixelStride * (size_t) dest.width);
|
|
}
|
|
else
|
|
{
|
|
if (auto* converter = converterFnTable.getConverterFor (src.pixelFormat, dest.pixelFormat))
|
|
converter (src, dest, dest.width, dest.height);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static Image convert (const Image::BitmapData& src, const ImageType& type)
|
|
{
|
|
Image result (type.create (src.pixelFormat, src.width, src.height, false));
|
|
Image::BitmapData (result, Image::BitmapData::writeOnly).convertFrom (src);
|
|
|
|
return result;
|
|
}
|
|
|
|
static void blurDataTriplets (uint8* d, int num, const int delta) noexcept
|
|
{
|
|
uint32 last = d[0];
|
|
d[0] = (uint8) ((d[0] + d[delta] + 1) / 3);
|
|
d += delta;
|
|
|
|
num -= 2;
|
|
|
|
do
|
|
{
|
|
const uint32 newLast = d[0];
|
|
d[0] = (uint8) ((last + d[0] + d[delta] + 1) / 3);
|
|
d += delta;
|
|
last = newLast;
|
|
}
|
|
while (--num > 0);
|
|
|
|
d[0] = (uint8) ((last + d[0] + 1) / 3);
|
|
}
|
|
|
|
static void blurSingleChannelImage (uint8* const data, const int w, const int h,
|
|
const int lineStride, const int repetitions) noexcept
|
|
{
|
|
jassert (w > 2 && h > 2);
|
|
|
|
for (int y = 0; y < h; ++y)
|
|
for (int i = repetitions; --i >= 0;)
|
|
blurDataTriplets (data + lineStride * y, w, 1);
|
|
|
|
for (int x = 0; x < w; ++x)
|
|
for (int i = repetitions; --i >= 0;)
|
|
blurDataTriplets (data + x, h, lineStride);
|
|
}
|
|
|
|
template <class PixelType>
|
|
struct PixelIterator
|
|
{
|
|
template <class PixelOperation>
|
|
static void iterate (const Image::BitmapData& data, const PixelOperation& pixelOp)
|
|
{
|
|
for (int y = 0; y < data.height; ++y)
|
|
for (int x = 0; x < data.width; ++x)
|
|
pixelOp (*reinterpret_cast<PixelType*> (data.getPixelPointer (x, y)));
|
|
}
|
|
};
|
|
|
|
template <class PixelOperation>
|
|
static void performPixelOp (const Image::BitmapData& data, const PixelOperation& pixelOp)
|
|
{
|
|
switch (data.pixelFormat)
|
|
{
|
|
case Image::ARGB: PixelIterator<PixelARGB> ::iterate (data, pixelOp); break;
|
|
case Image::RGB: PixelIterator<PixelRGB> ::iterate (data, pixelOp); break;
|
|
case Image::SingleChannel: PixelIterator<PixelAlpha>::iterate (data, pixelOp); break;
|
|
case Image::UnknownFormat:
|
|
default: jassertfalse; break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
class SubsectionPixelData : public ImagePixelData
|
|
{
|
|
public:
|
|
using Ptr = ReferenceCountedObjectPtr<SubsectionPixelData>;
|
|
|
|
SubsectionPixelData (ImagePixelData::Ptr source, Rectangle<int> r)
|
|
: ImagePixelData (source->pixelFormat, r.getWidth(), r.getHeight()),
|
|
sourceImage (std::move (source)),
|
|
area (r)
|
|
{
|
|
}
|
|
|
|
Rectangle<int> getSubsection() const { return area; }
|
|
ImagePixelData::Ptr getSourcePixelData() const { return sourceImage; }
|
|
|
|
std::unique_ptr<LowLevelGraphicsContext> createLowLevelContext() override
|
|
{
|
|
auto g = sourceImage->createLowLevelContext();
|
|
g->clipToRectangle (area);
|
|
g->setOrigin (area.getPosition());
|
|
return g;
|
|
}
|
|
|
|
void initialiseBitmapData (Image::BitmapData& bitmap, int x, int y, Image::BitmapData::ReadWriteMode mode) override
|
|
{
|
|
sourceImage->initialiseBitmapData (bitmap, x + area.getX(), y + area.getY(), mode);
|
|
|
|
if (mode != Image::BitmapData::readOnly)
|
|
sendDataChangeMessage();
|
|
}
|
|
|
|
ImagePixelData::Ptr clone() override
|
|
{
|
|
jassert (getReferenceCount() > 0); // (This method can't be used on an unowned pointer, as it will end up self-deleting)
|
|
auto type = createType();
|
|
auto result = type->create (pixelFormat, area.getWidth(), area.getHeight(), pixelFormat != Image::RGB);
|
|
|
|
{
|
|
Graphics g { Image { result } };
|
|
g.drawImageAt (Image (*this), 0, 0);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::unique_ptr<ImageType> createType() const override { return sourceImage->createType(); }
|
|
|
|
void applySingleChannelBoxBlurEffectInArea (Rectangle<int> b, int radius) override
|
|
{
|
|
sourceImage->applySingleChannelBoxBlurEffectInArea (getIntersection (b), radius);
|
|
}
|
|
|
|
void applyGaussianBlurEffectInArea (Rectangle<int> b, float radius) override
|
|
{
|
|
sourceImage->applyGaussianBlurEffectInArea (getIntersection (b), radius);
|
|
}
|
|
|
|
void multiplyAllAlphasInArea (Rectangle<int> b, float amount) override
|
|
{
|
|
sourceImage->multiplyAllAlphasInArea (getIntersection (b), amount);
|
|
}
|
|
|
|
void desaturateInArea (Rectangle<int> b) override
|
|
{
|
|
sourceImage->desaturateInArea (getIntersection (b));
|
|
}
|
|
|
|
/* as we always hold a reference to image, don't double count */
|
|
int getSharedCount() const noexcept override { return getReferenceCount() + sourceImage->getSharedCount() - 1; }
|
|
|
|
NativeExtensions getNativeExtensions() override
|
|
{
|
|
struct Wrapped
|
|
{
|
|
explicit Wrapped (Ptr selfIn)
|
|
: self (selfIn) {}
|
|
|
|
#if JUCE_WINDOWS
|
|
Span<const Direct2DPixelDataPage> getPages (ComSmartPtr<ID2D1Device1> x) const
|
|
{
|
|
return self->sourceImage->getNativeExtensions().getPages (x);
|
|
}
|
|
#endif
|
|
|
|
#if JUCE_MAC || JUCE_IOS
|
|
CGContextRef getCGContext() const
|
|
{
|
|
return self->sourceImage->getNativeExtensions().getCGContext();
|
|
}
|
|
|
|
CFUniquePtr<CGImageRef> getCGImage (CGColorSpaceRef colourSpace) const
|
|
{
|
|
const auto& parentNative = self->sourceImage->getNativeExtensions();
|
|
const auto parentImage = parentNative.getCGImage (colourSpace);
|
|
return CFUniquePtr<CGImageRef> { CGImageCreateWithImageInRect (parentImage.get(),
|
|
makeCGRect (self->area + parentNative.getTopLeft())) };
|
|
}
|
|
#endif
|
|
|
|
Point<int> getTopLeft() const
|
|
{
|
|
return self->sourceImage->getNativeExtensions().getTopLeft() + self->area.getTopLeft();
|
|
}
|
|
|
|
Ptr self;
|
|
};
|
|
|
|
return NativeExtensions { Wrapped { this } };
|
|
}
|
|
|
|
private:
|
|
Rectangle<int> getIntersection (Rectangle<int> b) const
|
|
{
|
|
return area.getIntersection (b + area.getTopLeft());
|
|
}
|
|
|
|
friend class Image;
|
|
const ImagePixelData::Ptr sourceImage;
|
|
const Rectangle<int> area;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SubsectionPixelData)
|
|
};
|
|
|
|
//==============================================================================
|
|
ImagePixelData::ImagePixelData (Image::PixelFormat format, int w, int h)
|
|
: pixelFormat (format), width (w), height (h)
|
|
{
|
|
jassert (format == Image::RGB || format == Image::ARGB || format == Image::SingleChannel);
|
|
jassert (w > 0 && h > 0); // It's illegal to create a zero-sized image!
|
|
}
|
|
|
|
ImagePixelData::~ImagePixelData()
|
|
{
|
|
listeners.call ([this] (Listener& l) { l.imageDataBeingDeleted (this); });
|
|
}
|
|
|
|
void ImagePixelData::sendDataChangeMessage()
|
|
{
|
|
listeners.call ([this] (Listener& l) { l.imageDataChanged (this); });
|
|
}
|
|
|
|
int ImagePixelData::getSharedCount() const noexcept
|
|
{
|
|
return getReferenceCount();
|
|
}
|
|
|
|
void ImagePixelData::applySingleChannelBoxBlurEffectInArea (Rectangle<int> bounds, int radius)
|
|
{
|
|
if (pixelFormat == Image::SingleChannel)
|
|
{
|
|
const Image::BitmapData bm (Image { this }, bounds, Image::BitmapData::readWrite);
|
|
BitmapDataDetail::blurSingleChannelImage (bm.data, bm.width, bm.height, bm.lineStride, 2 * radius);
|
|
}
|
|
}
|
|
|
|
void ImagePixelData::applyGaussianBlurEffectInArea (Rectangle<int> bounds, float radius)
|
|
{
|
|
ImageConvolutionKernel blurKernel (roundToInt (radius * 2.0f));
|
|
blurKernel.createGaussianBlur (radius);
|
|
|
|
Image target { this };
|
|
blurKernel.applyToImage (target, Image { this }.createCopy(), bounds);
|
|
}
|
|
|
|
void ImagePixelData::multiplyAllAlphasInArea (Rectangle<int> b, float amount)
|
|
{
|
|
if (pixelFormat == Image::ARGB || pixelFormat == Image::SingleChannel)
|
|
{
|
|
const Image::BitmapData destData (Image { this }, b, Image::BitmapData::readWrite);
|
|
BitmapDataDetail::performPixelOp (destData, [&] (auto& p) { p.multiplyAlpha (amount); });
|
|
}
|
|
}
|
|
|
|
void ImagePixelData::desaturateInArea (Rectangle<int> b)
|
|
{
|
|
if (pixelFormat == Image::ARGB || pixelFormat == Image::RGB)
|
|
{
|
|
const Image::BitmapData destData (Image { this }, b, Image::BitmapData::readWrite);
|
|
BitmapDataDetail::performPixelOp (destData, [] (auto& p) { p.desaturate(); });
|
|
}
|
|
}
|
|
|
|
auto ImagePixelData::getNativeExtensions() -> NativeExtensions
|
|
{
|
|
struct Wrapped
|
|
{
|
|
Point<int> getTopLeft() const { return {}; }
|
|
|
|
#if JUCE_WINDOWS
|
|
Span<const Direct2DPixelDataPage> getPages (ComSmartPtr<ID2D1Device1>) const { return {}; }
|
|
#endif
|
|
|
|
#if JUCE_MAC || JUCE_IOS
|
|
CGContextRef getCGContext() const { return {}; }
|
|
CFUniquePtr<CGImageRef> getCGImage (CGColorSpaceRef) const { return {}; }
|
|
#endif
|
|
};
|
|
|
|
return NativeExtensions { Wrapped{} };
|
|
}
|
|
|
|
struct MoveImageParams
|
|
{
|
|
Rectangle<int> src{};
|
|
Point<int> dst{};
|
|
|
|
MoveImageParams constrained (int width, int height) const
|
|
{
|
|
const auto intersectedSrc = src.getIntersection ({ width, height });
|
|
const auto srcOffset = intersectedSrc.getPosition() - src.getPosition();
|
|
const auto intersectedDst = intersectedSrc.withPosition (dst + srcOffset).getIntersection ({ width, height });
|
|
|
|
if (intersectedDst.isEmpty())
|
|
return {};
|
|
|
|
const MoveImageParams result { intersectedDst.withPosition (intersectedDst.getPosition() + src.getPosition() - dst),
|
|
intersectedDst.getPosition() };
|
|
|
|
// postconditions
|
|
jassert ((juce::Rectangle { width, height }.contains (result.src)));
|
|
jassert ((juce::Rectangle { width, height }.contains (result.src.withPosition (result.dst))));
|
|
|
|
return result;
|
|
}
|
|
|
|
bool operator== (const MoveImageParams& other) const
|
|
{
|
|
const auto tie = [] (auto& x) { return std::tuple (x.src, x.dst); };
|
|
return tie (*this) == tie (other);
|
|
}
|
|
|
|
bool operator!= (const MoveImageParams& other) const
|
|
{
|
|
return ! operator== (other);
|
|
}
|
|
};
|
|
|
|
static void moveValidatedImageSectionInSoftware (ImagePixelData& img,
|
|
Point<int> destTopLeft,
|
|
Rectangle<int> sourceRect)
|
|
{
|
|
const auto minX = jmin (destTopLeft.x, sourceRect.getX());
|
|
const auto minY = jmin (destTopLeft.y, sourceRect.getY());
|
|
|
|
Image tempImage { &img };
|
|
const Image::BitmapData destData (tempImage,
|
|
minX,
|
|
minY,
|
|
sourceRect.getWidth(),
|
|
sourceRect.getHeight(),
|
|
Image::BitmapData::readWrite);
|
|
|
|
const auto dstPos = destTopLeft - Point { minX, minY };
|
|
const auto srcPos = sourceRect.getPosition() - Point { minX, minY };
|
|
|
|
auto* dst = destData.getPixelPointer (dstPos.x, dstPos.y);
|
|
const auto* src = destData.getPixelPointer (srcPos.x, srcPos.y);
|
|
|
|
const auto lineSize = (size_t) destData.pixelStride * (size_t) sourceRect.getWidth();
|
|
|
|
if (destTopLeft.y > sourceRect.getY())
|
|
{
|
|
for (auto h = sourceRect.getHeight(); --h >= 0;)
|
|
{
|
|
const auto offset = h * destData.lineStride;
|
|
memmove (dst + offset, src + offset, lineSize);
|
|
}
|
|
}
|
|
else if (dst != src)
|
|
{
|
|
for (auto h = sourceRect.getHeight(); --h >= 0;)
|
|
{
|
|
memmove (dst, src, lineSize);
|
|
dst += destData.lineStride;
|
|
src += destData.lineStride;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ImagePixelData::moveImageSection (Point<int> destTopLeft, Rectangle<int> sourceRect)
|
|
{
|
|
const auto constrained = MoveImageParams { sourceRect, destTopLeft }.constrained (width, height);
|
|
|
|
if (! constrained.src.isEmpty())
|
|
moveValidatedImageSection (constrained.dst, constrained.src);
|
|
}
|
|
|
|
void ImagePixelData::moveValidatedImageSection (Point<int> destTopLeft, Rectangle<int> sourceRect)
|
|
{
|
|
moveValidatedImageSectionInSoftware (*this, destTopLeft, sourceRect);
|
|
}
|
|
|
|
//==============================================================================
|
|
ImageType::ImageType() = default;
|
|
ImageType::~ImageType() = default;
|
|
|
|
Image ImageType::convert (const Image& source) const
|
|
{
|
|
if (source.isNull() || getTypeID() == source.getPixelData()->createType()->getTypeID())
|
|
return source;
|
|
|
|
const Image::BitmapData src (source, Image::BitmapData::readOnly);
|
|
|
|
if (src.data == nullptr)
|
|
return {};
|
|
|
|
return BitmapDataDetail::convert (src, *this);
|
|
}
|
|
|
|
//==============================================================================
|
|
class SoftwarePixelData : public ImagePixelData
|
|
{
|
|
public:
|
|
SoftwarePixelData (Image::PixelFormat formatToUse, int w, int h, bool clearImage)
|
|
: ImagePixelData (formatToUse, w, h),
|
|
pixelStride (formatToUse == Image::RGB ? 3 : ((formatToUse == Image::ARGB) ? 4 : 1)),
|
|
lineStride ((pixelStride * jmax (1, w) + 3) & ~3)
|
|
{
|
|
imageData.allocate ((size_t) lineStride * (size_t) jmax (1, h), clearImage);
|
|
}
|
|
|
|
std::unique_ptr<LowLevelGraphicsContext> createLowLevelContext() override
|
|
{
|
|
sendDataChangeMessage();
|
|
return std::make_unique<LowLevelGraphicsSoftwareRenderer> (Image (*this));
|
|
}
|
|
|
|
void initialiseBitmapData (Image::BitmapData& bitmap, int x, int y, Image::BitmapData::ReadWriteMode mode) override
|
|
{
|
|
const auto offset = (size_t) x * (size_t) pixelStride + (size_t) y * (size_t) lineStride;
|
|
bitmap.data = imageData + offset;
|
|
bitmap.size = (size_t) (height * lineStride) - offset;
|
|
bitmap.pixelFormat = pixelFormat;
|
|
bitmap.lineStride = lineStride;
|
|
bitmap.pixelStride = pixelStride;
|
|
|
|
if (mode != Image::BitmapData::readOnly)
|
|
sendDataChangeMessage();
|
|
}
|
|
|
|
Ptr clone() override
|
|
{
|
|
auto s = new SoftwarePixelData (pixelFormat, width, height, false);
|
|
memcpy (s->imageData, imageData, (size_t) lineStride * (size_t) height);
|
|
return *s;
|
|
}
|
|
|
|
std::unique_ptr<ImageType> createType() const override { return std::make_unique<SoftwareImageType>(); }
|
|
|
|
private:
|
|
HeapBlock<uint8> imageData;
|
|
const int pixelStride, lineStride;
|
|
|
|
JUCE_LEAK_DETECTOR (SoftwarePixelData)
|
|
};
|
|
|
|
SoftwareImageType::SoftwareImageType() = default;
|
|
SoftwareImageType::~SoftwareImageType() = default;
|
|
|
|
ImagePixelData::Ptr SoftwareImageType::create (Image::PixelFormat format, int width, int height, bool clearImage) const
|
|
{
|
|
return *new SoftwarePixelData (format, width, height, clearImage);
|
|
}
|
|
|
|
int SoftwareImageType::getTypeID() const
|
|
{
|
|
return 2;
|
|
}
|
|
|
|
//==============================================================================
|
|
NativeImageType::NativeImageType() = default;
|
|
NativeImageType::~NativeImageType() = default;
|
|
|
|
int NativeImageType::getTypeID() const
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
#if JUCE_LINUX || JUCE_BSD
|
|
ImagePixelData::Ptr NativeImageType::create (Image::PixelFormat format, int width, int height, bool clearImage) const
|
|
{
|
|
return new SoftwarePixelData (format, width, height, clearImage);
|
|
}
|
|
#endif
|
|
|
|
//==============================================================================
|
|
|
|
Image Image::getClippedImage (const Rectangle<int>& area) const
|
|
{
|
|
if (area.contains (getBounds()))
|
|
return *this;
|
|
|
|
auto validArea = area.getIntersection (getBounds());
|
|
|
|
if (validArea.isEmpty())
|
|
return {};
|
|
|
|
return Image { ImagePixelData::Ptr { new SubsectionPixelData { image, validArea } } };
|
|
}
|
|
|
|
//==============================================================================
|
|
Image::Image() noexcept = default;
|
|
|
|
Image::Image (ReferenceCountedObjectPtr<ImagePixelData> instance) noexcept
|
|
: image (std::move (instance))
|
|
{
|
|
}
|
|
|
|
Image::Image (PixelFormat format, int width, int height, bool clearImage)
|
|
: image (NativeImageType().create (format, width, height, clearImage))
|
|
{
|
|
}
|
|
|
|
Image::Image (PixelFormat format, int width, int height, bool clearImage, const ImageType& type)
|
|
: image (type.create (format, width, height, clearImage))
|
|
{
|
|
}
|
|
|
|
Image::Image (const Image& other) noexcept
|
|
: image (other.image)
|
|
{
|
|
}
|
|
|
|
Image& Image::operator= (const Image& other)
|
|
{
|
|
image = other.image;
|
|
return *this;
|
|
}
|
|
|
|
Image::Image (Image&& other) noexcept
|
|
: image (std::move (other.image))
|
|
{
|
|
}
|
|
|
|
Image& Image::operator= (Image&& other) noexcept
|
|
{
|
|
image = std::move (other.image);
|
|
return *this;
|
|
}
|
|
|
|
Image::~Image() = default;
|
|
|
|
int Image::getReferenceCount() const noexcept { return image == nullptr ? 0 : image->getSharedCount(); }
|
|
|
|
bool Image::isValid() const noexcept
|
|
{
|
|
return image != nullptr;
|
|
}
|
|
|
|
int Image::getWidth() const noexcept { return image == nullptr ? 0 : image->width; }
|
|
int Image::getHeight() const noexcept { return image == nullptr ? 0 : image->height; }
|
|
Rectangle<int> Image::getBounds() const noexcept { return image == nullptr ? Rectangle<int>() : Rectangle<int> (image->width, image->height); }
|
|
Image::PixelFormat Image::getFormat() const noexcept { return image == nullptr ? UnknownFormat : image->pixelFormat; }
|
|
bool Image::isARGB() const noexcept { return getFormat() == ARGB; }
|
|
bool Image::isRGB() const noexcept { return getFormat() == RGB; }
|
|
bool Image::isSingleChannel() const noexcept { return getFormat() == SingleChannel; }
|
|
bool Image::hasAlphaChannel() const noexcept { return getFormat() != RGB; }
|
|
|
|
std::unique_ptr<LowLevelGraphicsContext> Image::createLowLevelContext() const
|
|
{
|
|
if (image != nullptr)
|
|
return image->createLowLevelContext();
|
|
|
|
return {};
|
|
}
|
|
|
|
void Image::duplicateIfShared()
|
|
{
|
|
if (getReferenceCount() > 1)
|
|
image = image->clone();
|
|
}
|
|
|
|
Image Image::createCopy() const
|
|
{
|
|
if (image != nullptr)
|
|
return Image (image->clone());
|
|
|
|
return {};
|
|
}
|
|
|
|
Image Image::rescaled (int newWidth, int newHeight, Graphics::ResamplingQuality quality) const
|
|
{
|
|
if (image == nullptr || (image->width == newWidth && image->height == newHeight))
|
|
return *this;
|
|
|
|
auto type = image->createType();
|
|
Image newImage (type->create (image->pixelFormat, newWidth, newHeight, hasAlphaChannel()));
|
|
|
|
Graphics g (newImage);
|
|
g.setImageResamplingQuality (quality);
|
|
g.drawImageTransformed (*this, AffineTransform::scale ((float) newWidth / (float) image->width,
|
|
(float) newHeight / (float) image->height), false);
|
|
return newImage;
|
|
}
|
|
|
|
Image Image::convertedToFormat (PixelFormat newFormat) const
|
|
{
|
|
if (image == nullptr || newFormat == image->pixelFormat)
|
|
return *this;
|
|
|
|
auto w = image->width, h = image->height;
|
|
|
|
auto type = image->createType();
|
|
Image newImage (type->create (newFormat, w, h, false));
|
|
|
|
if (newImage.getFormat() == SingleChannel)
|
|
{
|
|
if (! hasAlphaChannel())
|
|
{
|
|
newImage.clear (getBounds(), Colours::black);
|
|
}
|
|
else
|
|
{
|
|
const BitmapData destData (newImage, { w, h }, BitmapData::writeOnly);
|
|
const BitmapData srcData (*this, { w, h }, BitmapData::readOnly);
|
|
|
|
for (int y = 0; y < h; ++y)
|
|
{
|
|
for (int x = 0; x < w; ++x)
|
|
{
|
|
auto* dstPtr = reinterpret_cast<PixelAlpha*> (destData.getPixelPointer (x, y));
|
|
auto* srcPtr = reinterpret_cast<const PixelARGB*> (srcData.getPixelPointer (x, y));
|
|
dstPtr->set (*srcPtr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (image->pixelFormat == SingleChannel && newImage.getFormat() == ARGB)
|
|
{
|
|
const BitmapData destData (newImage, { w, h }, BitmapData::writeOnly);
|
|
const BitmapData srcData (*this, { w, h }, BitmapData::readOnly);
|
|
|
|
for (int y = 0; y < h; ++y)
|
|
{
|
|
for (int x = 0; x < w; ++x)
|
|
{
|
|
auto* dstPtr = reinterpret_cast<PixelARGB*> (destData.getPixelPointer (x, y));
|
|
auto* srcPtr = reinterpret_cast<const PixelAlpha*> (srcData.getPixelPointer (x, y));
|
|
dstPtr->set (*srcPtr);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (hasAlphaChannel())
|
|
newImage.clear (getBounds());
|
|
|
|
Graphics g (newImage);
|
|
g.drawImageAt (*this, 0, 0);
|
|
}
|
|
|
|
return newImage;
|
|
}
|
|
|
|
NamedValueSet* Image::getProperties() const
|
|
{
|
|
return image == nullptr ? nullptr : &(image->userData);
|
|
}
|
|
|
|
//==============================================================================
|
|
Image::BitmapData::BitmapData (Image& im, int x, int y, int w, int h, ReadWriteMode mode)
|
|
: BitmapData (im, { x, y, w, h }, mode)
|
|
{
|
|
}
|
|
|
|
Image::BitmapData::BitmapData (const Image& im, Rectangle<int> bounds, ReadWriteMode mode)
|
|
: width (bounds.getWidth()), height (bounds.getHeight())
|
|
{
|
|
// The BitmapData class must be given a valid image, and a valid rectangle within it!
|
|
jassert (im.image != nullptr);
|
|
jassert (bounds.getX() >= 0);
|
|
jassert (bounds.getY() >= 0);
|
|
jassert (bounds.getWidth() > 0);
|
|
jassert (bounds.getHeight() > 0);
|
|
jassert (bounds.getRight() <= im.getWidth());
|
|
jassert (bounds.getBottom() <= im.getHeight());
|
|
|
|
im.image->initialiseBitmapData (*this, bounds.getX(), bounds.getY(), mode);
|
|
jassert (data != nullptr && pixelStride > 0 && lineStride != 0);
|
|
}
|
|
|
|
Image::BitmapData::BitmapData (const Image& im, int x, int y, int w, int h)
|
|
: width (w), height (h)
|
|
{
|
|
// The BitmapData class must be given a valid image, and a valid rectangle within it!
|
|
jassert (im.image != nullptr);
|
|
jassert (x >= 0 && y >= 0 && w > 0 && h > 0 && x + w <= im.getWidth() && y + h <= im.getHeight());
|
|
|
|
im.image->initialiseBitmapData (*this, x, y, readOnly);
|
|
jassert (data != nullptr && pixelStride > 0 && lineStride != 0);
|
|
}
|
|
|
|
Image::BitmapData::BitmapData (const Image& im, ReadWriteMode mode)
|
|
: width (im.getWidth()),
|
|
height (im.getHeight())
|
|
{
|
|
// The BitmapData class must be given a valid image!
|
|
jassert (im.image != nullptr);
|
|
|
|
im.image->initialiseBitmapData (*this, 0, 0, mode);
|
|
jassert (data != nullptr && pixelStride > 0 && lineStride != 0);
|
|
}
|
|
|
|
Colour Image::BitmapData::getPixelColour (int x, int y) const noexcept
|
|
{
|
|
auto* pixel = getPixelPointer (x, y);
|
|
|
|
switch (pixelFormat)
|
|
{
|
|
case ARGB: return BitmapDataDetail::getPixelColour (pixel, BitmapDataDetail::ARGB{});
|
|
case RGB: return BitmapDataDetail::getPixelColour (pixel, BitmapDataDetail::RGB{});
|
|
case SingleChannel: return BitmapDataDetail::getPixelColour (pixel, BitmapDataDetail::A{});
|
|
case UnknownFormat:
|
|
default: jassertfalse; break;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void Image::BitmapData::setPixelColour (int x, int y, Colour colour) const noexcept
|
|
{
|
|
auto* pixel = getPixelPointer (x, y);
|
|
auto col = colour.getPixelARGB();
|
|
|
|
switch (pixelFormat)
|
|
{
|
|
case ARGB: return BitmapDataDetail::setPixelColour (pixel, col, BitmapDataDetail::ARGB{});
|
|
case RGB: return BitmapDataDetail::setPixelColour (pixel, col, BitmapDataDetail::RGB{});
|
|
case SingleChannel: return BitmapDataDetail::setPixelColour (pixel, col, BitmapDataDetail::A{});
|
|
case UnknownFormat:
|
|
default: jassertfalse; break;
|
|
}
|
|
}
|
|
|
|
bool Image::BitmapData::convertFrom (const BitmapData& source)
|
|
{
|
|
return BitmapDataDetail::convert (source, *this);
|
|
}
|
|
|
|
bool Image::setBackupEnabled (bool enabled)
|
|
{
|
|
if (auto ptr = image)
|
|
{
|
|
if (auto* ext = ptr->getBackupExtensions())
|
|
{
|
|
ext->setBackupEnabled (enabled);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//==============================================================================
|
|
void Image::clear (const Rectangle<int>& area, Colour colourToClearTo)
|
|
{
|
|
if (image == nullptr)
|
|
return;
|
|
|
|
auto g = image->createLowLevelContext();
|
|
g->setFill (colourToClearTo);
|
|
g->fillRect (area, true);
|
|
}
|
|
|
|
//==============================================================================
|
|
Colour Image::getPixelAt (int x, int y) const
|
|
{
|
|
if (isPositiveAndBelow (x, getWidth()) && isPositiveAndBelow (y, getHeight()))
|
|
{
|
|
const BitmapData srcData (*this, x, y, 1, 1);
|
|
return srcData.getPixelColour (0, 0);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void Image::setPixelAt (int x, int y, Colour colour)
|
|
{
|
|
if (isPositiveAndBelow (x, getWidth()) && isPositiveAndBelow (y, getHeight()))
|
|
{
|
|
const BitmapData destData (*this, x, y, 1, 1, BitmapData::writeOnly);
|
|
destData.setPixelColour (0, 0, colour);
|
|
}
|
|
}
|
|
|
|
void Image::multiplyAlphaAt (int x, int y, float multiplier)
|
|
{
|
|
if (isPositiveAndBelow (x, getWidth()) && isPositiveAndBelow (y, getHeight())
|
|
&& hasAlphaChannel())
|
|
{
|
|
const BitmapData destData (*this, x, y, 1, 1, BitmapData::readWrite);
|
|
|
|
if (isARGB())
|
|
reinterpret_cast<PixelARGB*> (destData.data)->multiplyAlpha (multiplier);
|
|
else
|
|
*(destData.data) = (uint8) (*(destData.data) * multiplier);
|
|
}
|
|
}
|
|
|
|
void Image::multiplyAllAlphas (float amountToMultiplyBy)
|
|
{
|
|
if (auto ptr = image)
|
|
ptr->multiplyAllAlphas (amountToMultiplyBy);
|
|
}
|
|
|
|
void Image::desaturate()
|
|
{
|
|
if (auto ptr = image)
|
|
ptr->desaturate();
|
|
}
|
|
|
|
void Image::createSolidAreaMask (RectangleList<int>& result, float alphaThreshold) const
|
|
{
|
|
if (hasAlphaChannel())
|
|
{
|
|
auto threshold = (uint8) jlimit (0, 255, roundToInt (alphaThreshold * 255.0f));
|
|
SparseSet<int> pixelsOnRow;
|
|
|
|
const BitmapData srcData (*this, 0, 0, getWidth(), getHeight());
|
|
|
|
for (int y = 0; y < srcData.height; ++y)
|
|
{
|
|
pixelsOnRow.clear();
|
|
auto lineData = srcData.getLinePointer (y);
|
|
|
|
if (isARGB())
|
|
{
|
|
for (int x = 0; x < srcData.width; ++x)
|
|
{
|
|
if (reinterpret_cast<const PixelARGB*> (lineData)->getAlpha() >= threshold)
|
|
pixelsOnRow.addRange (Range<int> (x, x + 1));
|
|
|
|
lineData += srcData.pixelStride;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int x = 0; x < srcData.width; ++x)
|
|
{
|
|
if (*lineData >= threshold)
|
|
pixelsOnRow.addRange (Range<int> (x, x + 1));
|
|
|
|
lineData += srcData.pixelStride;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < pixelsOnRow.getNumRanges(); ++i)
|
|
{
|
|
auto range = pixelsOnRow.getRange (i);
|
|
result.add (Rectangle<int> (range.getStart(), y, range.getLength(), 1));
|
|
}
|
|
|
|
result.consolidate();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.add (0, 0, getWidth(), getHeight());
|
|
}
|
|
}
|
|
|
|
void Image::moveImageSection (int dx, int dy, int sx, int sy, int w, int h)
|
|
{
|
|
if (image != nullptr)
|
|
image->moveImageSection ({ dx, dy }, { sx, sy, w, h });
|
|
}
|
|
|
|
//==============================================================================
|
|
#if JUCE_ALLOW_STATIC_NULL_VARIABLES
|
|
|
|
JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS
|
|
|
|
const Image Image::null;
|
|
|
|
JUCE_END_IGNORE_DEPRECATION_WARNINGS
|
|
|
|
#endif
|
|
|
|
#if JUCE_UNIT_TESTS
|
|
|
|
class ImagePixelDataClippingTests : public UnitTest
|
|
{
|
|
public:
|
|
ImagePixelDataClippingTests()
|
|
: UnitTest ("ImagePixelDataClippingTests", UnitTestCategories::graphics)
|
|
{
|
|
}
|
|
|
|
void runTest() override
|
|
{
|
|
beginTest ("MoveImageParams constrains arguments appropriately");
|
|
{
|
|
expect ((MoveImageParams { { 300, 400 }, { 5, 5 } }.constrained (350, 450) == MoveImageParams { { 300, 400 }, { 5, 5 } }));
|
|
expect ((MoveImageParams { { 350, 450 }, { 5, 5 } }.constrained (300, 400) == MoveImageParams { { 295, 395 }, { 5, 5 } }));
|
|
expect ((MoveImageParams { { -5, -10, 20, 30 }, { 0, 0 } }.constrained (100, 100) == MoveImageParams { { 15, 20 }, { 5, 10 } }));
|
|
expect ((MoveImageParams { { 1, 2, 10, 10 }, { -5, -5 } }.constrained (20, 20) == MoveImageParams { { 6, 7, 5, 5 }, { 0, 0 } }));
|
|
expect ((MoveImageParams { { 40, 50, 100, 100 }, { 10, 10 } }.constrained (100, 100) == MoveImageParams { { 40, 50, 60, 50 }, { 10, 10 } }));
|
|
expect ((MoveImageParams { { 20, 20, 10, 10 }, { -20, -20 } }.constrained (20, 20) == MoveImageParams { { 0, 0, 0, 0 }, { 0, 0 } }));
|
|
expect ((MoveImageParams { { -20, -30, 100, 100 }, { -30, -40 } }.constrained (100, 100) == MoveImageParams { { 10, 10, 70, 60 }, { 0, 0 } }));
|
|
}
|
|
}
|
|
};
|
|
|
|
static ImagePixelDataClippingTests imagePixelDataClippingTests;
|
|
|
|
#endif
|
|
|
|
} // namespace juce
|