/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce::build_tools { Array asArray (const Icons& icons) { Array result; if (icons.small != nullptr) result.add (icons.small.get()); if (icons.big != nullptr) result.add (icons.big.get()); return result; } namespace mac { static Image fixIconImageSize (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::max(); 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 = [&]() -> Drawable* { if ((icons.small != nullptr) != (icons.big != nullptr)) return icons.small != nullptr ? icons.small.get() : icons.big.get(); if (icons.small != nullptr && icons.big != nullptr) { if (icons.small->getWidth() >= size && icons.big->getWidth() >= size) return icons.small->getWidth() < icons.big->getWidth() ? icons.small.get() : icons.big.get(); if (icons.small->getWidth() >= size) return icons.small.get(); if (icons.big->getWidth() >= size) return icons.big.get(); } 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& 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 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 (Drawable& d, const int size) { if (auto* drawableImage = dynamic_cast (&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.big != nullptr ? icons.big.get() : icons.small.get(); 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 }; } } // namespace juce::build_tools