1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00
JUCE/extras/Build/juce_build_tools/utils/juce_Icons.cpp

595 lines
22 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::build_tools
{
Icons Icons::fromFilesSmallAndBig (const File& small, const File& big)
{
Icons result;
result.small = Drawable::createFromImageFile (small);
result.big = Drawable::createFromImageFile (big);
return result;
}
Array<const Drawable*> asArray (const Icons& icons)
{
Array<const Drawable*> result;
for (auto getter : { &Icons::getSmall, &Icons::getBig })
if (auto* got = (icons.*getter)())
result.add (got);
return result;
}
namespace mac
{
static Image fixIconImageSize (const Drawable& image)
{
const int validSizes[] = { 16, 32, 64, 128, 256, 512, 1024 };
auto w = image.getWidth();
auto h = image.getHeight();
int bestSize = 16;
for (int size : validSizes)
{
if (w == h && w == size)
{
bestSize = w;
break;
}
if (jmax (w, h) > size)
bestSize = size;
}
return rescaleImageForIcon (image, bestSize);
}
static void writeIconData (MemoryOutputStream& out, const Image& image, const char* type)
{
MemoryOutputStream pngData;
PNGImageFormat pngFormat;
pngFormat.writeImageToStream (image, pngData);
out.write (type, 4);
out.writeIntBigEndian (8 + (int) pngData.getDataSize());
out << pngData;
}
} // namespace mac
static void writeMacIcon (const Icons& icons, OutputStream& out)
{
MemoryOutputStream data;
auto smallest = std::numeric_limits<int>::max();
const Drawable* smallestImage = nullptr;
const auto images = asArray (icons);
for (int i = 0; i < images.size(); ++i)
{
auto image = mac::fixIconImageSize (*images[i]);
jassert (image.getWidth() == image.getHeight());
if (image.getWidth() < smallest)
{
smallest = image.getWidth();
smallestImage = images[i];
}
switch (image.getWidth())
{
case 16: mac::writeIconData (data, image, "icp4"); break;
case 32: mac::writeIconData (data, image, "icp5"); break;
case 64: mac::writeIconData (data, image, "icp6"); break;
case 128: mac::writeIconData (data, image, "ic07"); break;
case 256: mac::writeIconData (data, image, "ic08"); break;
case 512: mac::writeIconData (data, image, "ic09"); break;
case 1024: mac::writeIconData (data, image, "ic10"); break;
default: break;
}
}
jassert (data.getDataSize() > 0); // no suitable sized images?
// If you only supply a 1024 image, the file doesn't work on 10.8, so we need
// to force a smaller one in there too..
if (smallest > 512 && smallestImage != nullptr)
mac::writeIconData (data, rescaleImageForIcon (*smallestImage, 512), "ic09");
out.write ("icns", 4);
out.writeIntBigEndian ((int) data.getDataSize() + 8);
out << data;
}
Image getBestIconForSize (const Icons& icons,
int size,
bool returnNullIfNothingBigEnough)
{
auto* const im = std::invoke ([&]() -> const Drawable*
{
if ((icons.getSmall() != nullptr) != (icons.getBig() != nullptr))
return icons.getSmall() != nullptr ? icons.getSmall() : icons.getBig();
if (icons.getSmall() != nullptr && icons.getBig() != nullptr)
{
if (icons.getSmall()->getWidth() >= size && icons.getBig()->getWidth() >= size)
return icons.getSmall()->getWidth() < icons.getBig()->getWidth() ? icons.getSmall() : icons.getBig();
if (icons.getSmall()->getWidth() >= size)
return icons.getSmall();
if (icons.getBig()->getWidth() >= size)
return icons.getBig();
}
return nullptr;
});
if (im == nullptr)
return {};
if (returnNullIfNothingBigEnough && im->getWidth() < size && im->getHeight() < size)
return {};
return rescaleImageForIcon (*im, size);
}
namespace win
{
static void writeBMPImage (const Image& image, const int w, const int h, MemoryOutputStream& out)
{
int maskStride = (w / 8 + 3) & ~3;
out.writeInt (40); // bitmapinfoheader size
out.writeInt (w);
out.writeInt (h * 2);
out.writeShort (1); // planes
out.writeShort (32); // bits
out.writeInt (0); // compression
out.writeInt ((h * w * 4) + (h * maskStride)); // size image
out.writeInt (0); // x pixels per meter
out.writeInt (0); // y pixels per meter
out.writeInt (0); // clr used
out.writeInt (0); // clr important
Image::BitmapData bitmap (image, Image::BitmapData::readOnly);
int alphaThreshold = 5;
int y;
for (y = h; --y >= 0;)
{
for (int x = 0; x < w; ++x)
{
auto pixel = bitmap.getPixelColour (x, y);
if (pixel.getAlpha() <= alphaThreshold)
{
out.writeInt (0);
}
else
{
out.writeByte ((char) pixel.getBlue());
out.writeByte ((char) pixel.getGreen());
out.writeByte ((char) pixel.getRed());
out.writeByte ((char) pixel.getAlpha());
}
}
}
for (y = h; --y >= 0;)
{
int mask = 0, count = 0;
for (int x = 0; x < w; ++x)
{
auto pixel = bitmap.getPixelColour (x, y);
mask <<= 1;
if (pixel.getAlpha() <= alphaThreshold)
mask |= 1;
if (++count == 8)
{
out.writeByte ((char) mask);
count = 0;
mask = 0;
}
}
if (mask != 0)
out.writeByte ((char) mask);
for (int i = maskStride - w / 8; --i >= 0;)
out.writeByte (0);
}
}
static void writeIcon (const Array<Image>& images, OutputStream& out)
{
out.writeShort (0); // reserved
out.writeShort (1); // .ico tag
out.writeShort ((short) images.size());
MemoryOutputStream dataBlock;
int imageDirEntrySize = 16;
int dataBlockStart = 6 + images.size() * imageDirEntrySize;
for (int i = 0; i < images.size(); ++i)
{
auto oldDataSize = dataBlock.getDataSize();
auto& image = images.getReference (i);
auto w = image.getWidth();
auto h = image.getHeight();
if (w >= 256 || h >= 256)
{
PNGImageFormat pngFormat;
pngFormat.writeImageToStream (image, dataBlock);
}
else
{
writeBMPImage (image, w, h, dataBlock);
}
out.writeByte ((char) w);
out.writeByte ((char) h);
out.writeByte (0);
out.writeByte (0);
out.writeShort (1); // colour planes
out.writeShort (32); // bits per pixel
out.writeInt ((int) (dataBlock.getDataSize() - oldDataSize));
out.writeInt (dataBlockStart + (int) oldDataSize);
}
jassert (out.getPosition() == dataBlockStart);
out << dataBlock;
}
} // namespace win
static void writeWinIcon (const Icons& icons, OutputStream& os)
{
Array<Image> images;
int sizes[] = { 16, 32, 48, 256 };
for (int size : sizes)
{
auto im = getBestIconForSize (icons, size, true);
if (im.isValid())
images.add (im);
}
if (images.size() > 0)
win::writeIcon (images, os);
}
void writeMacIcon (const Icons& icons, const File& file)
{
writeStreamToFile (file, [&] (MemoryOutputStream& mo) { writeMacIcon (icons, mo); });
}
void writeWinIcon (const Icons& icons, const File& file)
{
writeStreamToFile (file, [&] (MemoryOutputStream& mo) { writeWinIcon (icons, mo); });
}
Image rescaleImageForIcon (const Drawable& d, const int size)
{
if (auto* drawableImage = dynamic_cast<const DrawableImage*> (&d))
{
auto im = SoftwareImageType().convert (drawableImage->getImage());
if (im.getWidth() == size && im.getHeight() == size)
return im;
// (scale it down in stages for better resampling)
while (im.getWidth() > 2 * size && im.getHeight() > 2 * size)
im = im.rescaled (im.getWidth() / 2,
im.getHeight() / 2);
Image newIm (Image::ARGB, size, size, true, SoftwareImageType());
Graphics g (newIm);
g.drawImageWithin (im, 0, 0, size, size,
RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize, false);
return newIm;
}
Image im (Image::ARGB, size, size, true, SoftwareImageType());
Graphics g (im);
d.drawWithin (g, im.getBounds().toFloat(), RectanglePlacement::centred, 1.0f);
return im;
}
struct AppIconType
{
const char* idiom;
const char* sizeString;
const char* filename;
const char* scale;
int size;
};
static const AppIconType iOSAppIconTypes[]
{
{ "iphone", "20x20", "Icon-Notification-20@2x.png", "2x", 40 },
{ "iphone", "20x20", "Icon-Notification-20@3x.png", "3x", 60 },
{ "iphone", "29x29", "Icon-29.png", "1x", 29 },
{ "iphone", "29x29", "Icon-29@2x.png", "2x", 58 },
{ "iphone", "29x29", "Icon-29@3x.png", "3x", 87 },
{ "iphone", "40x40", "Icon-Spotlight-40@2x.png", "2x", 80 },
{ "iphone", "40x40", "Icon-Spotlight-40@3x.png", "3x", 120 },
{ "iphone", "60x60", "Icon-60@2x.png", "2x", 120 },
{ "iphone", "60x60", "Icon-@3x.png", "3x", 180 },
{ "ipad", "20x20", "Icon-Notifications-20.png", "1x", 20 },
{ "ipad", "20x20", "Icon-Notifications-20@2x.png", "2x", 40 },
{ "ipad", "29x29", "Icon-Small-1.png", "1x", 29 },
{ "ipad", "29x29", "Icon-Small@2x-1.png", "2x", 58 },
{ "ipad", "40x40", "Icon-Spotlight-40.png", "1x", 40 },
{ "ipad", "40x40", "Icon-Spotlight-40@2x-1.png", "2x", 80 },
{ "ipad", "76x76", "Icon-76.png", "1x", 76 },
{ "ipad", "76x76", "Icon-76@2x.png", "2x", 152 },
{ "ipad", "83.5x83.5", "Icon-83.5@2x.png", "2x", 167 },
{ "ios-marketing", "1024x1024", "Icon-AppStore-1024.png", "1x", 1024 }
};
static void createiOSIconFiles (const Icons& icons, File appIconSet)
{
auto* imageToUse = icons.getBig() != nullptr ? icons.getBig()
: icons.getSmall();
if (imageToUse != nullptr)
{
for (auto& type : iOSAppIconTypes)
{
auto image = rescaleImageForIcon (*imageToUse, type.size);
if (image.hasAlphaChannel())
{
Image background (Image::RGB, image.getWidth(), image.getHeight(), false);
Graphics g (background);
g.fillAll (Colours::white);
g.drawImageWithin (image, 0, 0, image.getWidth(), image.getHeight(),
RectanglePlacement::centred | RectanglePlacement::onlyReduceInSize);
image = background;
}
MemoryOutputStream pngData;
PNGImageFormat pngFormat;
pngFormat.writeImageToStream (image, pngData);
overwriteFileIfDifferentOrThrow (appIconSet.getChildFile (type.filename), pngData);
}
}
}
static String getiOSAssetContents (var images)
{
DynamicObject::Ptr v (new DynamicObject());
var info (new DynamicObject());
info.getDynamicObject()->setProperty ("version", 1);
info.getDynamicObject()->setProperty ("author", "xcode");
v->setProperty ("images", images);
v->setProperty ("info", info);
return JSON::toString (var (v.get()));
}
//==============================================================================
static String getiOSAppIconContents()
{
var images;
for (auto& type : iOSAppIconTypes)
{
DynamicObject::Ptr d (new DynamicObject());
d->setProperty ("idiom", type.idiom);
d->setProperty ("size", type.sizeString);
d->setProperty ("filename", type.filename);
d->setProperty ("scale", type.scale);
images.append (var (d.get()));
}
return getiOSAssetContents (images);
}
struct ImageType
{
const char* orientation;
const char* idiom;
const char* subtype;
const char* extent;
const char* scale;
const char* filename;
int width;
int height;
};
static const ImageType iOSLaunchImageTypes[]
{
{ "portrait", "iphone", nullptr, "full-screen", "2x", "LaunchImage-iphone-2x.png", 640, 960 },
{ "portrait", "iphone", "retina4", "full-screen", "2x", "LaunchImage-iphone-retina4.png", 640, 1136 },
{ "portrait", "ipad", nullptr, "full-screen", "1x", "LaunchImage-ipad-portrait-1x.png", 768, 1024 },
{ "landscape","ipad", nullptr, "full-screen", "1x", "LaunchImage-ipad-landscape-1x.png", 1024, 768 },
{ "portrait", "ipad", nullptr, "full-screen", "2x", "LaunchImage-ipad-portrait-2x.png", 1536, 2048 },
{ "landscape","ipad", nullptr, "full-screen", "2x", "LaunchImage-ipad-landscape-2x.png", 2048, 1536 }
};
static void createiOSLaunchImageFiles (const File& launchImageSet)
{
for (auto& type : iOSLaunchImageTypes)
{
Image image (Image::ARGB, type.width, type.height, true); // (empty black image)
image.clear (image.getBounds(), Colours::black);
MemoryOutputStream pngData;
PNGImageFormat pngFormat;
pngFormat.writeImageToStream (image, pngData);
build_tools::overwriteFileIfDifferentOrThrow (launchImageSet.getChildFile (type.filename), pngData);
}
}
static String getiOSLaunchImageContents()
{
var images;
for (auto& type : iOSLaunchImageTypes)
{
DynamicObject::Ptr d (new DynamicObject());
d->setProperty ("orientation", type.orientation);
d->setProperty ("idiom", type.idiom);
d->setProperty ("extent", type.extent);
d->setProperty ("minimum-system-version", "7.0");
d->setProperty ("scale", type.scale);
d->setProperty ("filename", type.filename);
if (type.subtype != nullptr)
d->setProperty ("subtype", type.subtype);
images.append (var (d.get()));
}
return getiOSAssetContents (images);
}
RelativePath createXcassetsFolderFromIcons (const Icons& icons,
const File& targetFolder,
String projectFilenameRootString)
{
const auto assets = targetFolder.getChildFile (projectFilenameRootString)
.getChildFile ("Images.xcassets");
const auto iconSet = assets.getChildFile ("AppIcon.appiconset");
const auto launchImage = assets.getChildFile ("LaunchImage.launchimage");
overwriteFileIfDifferentOrThrow (iconSet.getChildFile ("Contents.json"), getiOSAppIconContents());
createiOSIconFiles (icons, iconSet);
overwriteFileIfDifferentOrThrow (launchImage.getChildFile ("Contents.json"), getiOSLaunchImageContents());
createiOSLaunchImageFiles (launchImage);
return { assets, targetFolder, RelativePath::buildTargetFolder };
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class IconsUnitTests : public UnitTest
{
public:
IconsUnitTests() : UnitTest ("Generate icon files", UnitTestCategories::graphics) {}
void runTest() override
{
const ScopedJuceInitialiser_GUI scope;
beginTest ("Load icons from vector file");
{
TemporaryFile tempFile ("vector");
{
auto stream = tempFile.getFile().createOutputStream();
expect (stream != nullptr);
stream->write (svg, std::size (svg));
}
const auto icons = Icons::fromFilesSmallAndBig (tempFile.getFile(), {});
expect (icons.getSmall() != nullptr);
expect (icons.getBig() == nullptr);
expect (dynamic_cast<const DrawableImage*> (icons.getSmall()) == nullptr,
"Vector data should not be rasterised on load");
}
beginTest ("Load icons from raster file");
{
TemporaryFile tempFile ("raster");
{
auto stream = tempFile.getFile().createOutputStream();
expect (stream != nullptr);
stream->write (png, std::size (png));
}
const auto icons = Icons::fromFilesSmallAndBig ({}, tempFile.getFile());
expect (icons.getSmall() == nullptr);
expect (icons.getBig() != nullptr);
expect (dynamic_cast<const DrawableImage*> (icons.getBig()) != nullptr,
"Raster data is loaded as a DrawableImage");
}
}
private:
static constexpr uint8_t svg[] = R"svg(
<svg xmlns="http://www.w3.org/2000/svg" width="284.4" height="284.4" viewBox="0 0 284.4 284.4">
<g>
<ellipse cx="142.2" cy="142.2" rx="132.82" ry="132.74" fill="#fff"/>
</g>
</svg>)svg";
static constexpr uint8_t png[]
{
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x07,
0x08, 0x06, 0x00, 0x00, 0x00, 0xc4, 0x52, 0x57, 0xd3, 0x00, 0x00, 0x00,
0x5e, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x55, 0x8d, 0x49, 0x0e, 0x00,
0x21, 0x08, 0x04, 0xfd, 0x89, 0xe2, 0x12, 0x13, 0xf5, 0xea, 0xff, 0x7f,
0x46, 0x4b, 0x9b, 0xe8, 0x38, 0x87, 0x0a, 0x84, 0x5e, 0x70, 0x21, 0x04,
0x25, 0xde, 0x7b, 0xcd, 0x39, 0x6b, 0x4a, 0x69, 0xef, 0xc4, 0x89, 0x08,
0x48, 0xef, 0x1d, 0x63, 0x0c, 0xcc, 0x39, 0xd1, 0x5a, 0xe3, 0xed, 0x13,
0x2d, 0x71, 0xa1, 0xd1, 0x0c, 0xea, 0xac, 0x12, 0x31, 0x46, 0x58, 0xe5,
0x86, 0x22, 0x67, 0xad, 0xf5, 0x9f, 0x3c, 0x86, 0x52, 0x0a, 0xee, 0x4f,
0xa6, 0xdf, 0x6a, 0xee, 0x5b, 0x64, 0xe5, 0x49, 0xbf, 0x50, 0x5c, 0x2f,
0xb3, 0x44, 0xdf, 0x94, 0x9e, 0x62, 0xe2, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82
};
};
static IconsUnitTests iconsUnitTests;
#endif
} // namespace juce::build_tools