diff --git a/README.md b/README.md index 00d4654..5af1096 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,23 @@ Yet another Java launcher generator for Windows. Unique features are: * Knows about (jigsaw) modules. * Works with Java 10+. * Automatically creates the necessary icon resources from a single .png image. +* Offers a command line option to generate a Windows `.ico`-file from a given `.png`-file. -Many other fine launchers exist. But after a long search we did not find one that was able to work with Java 11 and application images generated by JLink. jlgen is in use today as part of our Windows toolchain for generating Java-JLink-based native Windows installers for a set of commercial applications. And no customer knows it's Java :) +Many other fine launchers exist. But after a long search we did not find one that was able to work with Java 11 and application images generated by JLink. jlgen is in use today as part of our Windows build chain for generating Java-JLink-based native Windows installers for a set of commercial applications. And no customer knows it's Java :) -## How to generate a launcher +In addition we had a hard time to generate Windows `.ico`-files. The existing tools didn't work or were clumsy to use, so we added this to `jlgen`. -Get the latest version of the jlgen executable from the project's [release page](https://github.com/michab66/jlaunch/releases). +# Usage -`jlgen.exe` is a command line application that supports different commands. The command *`MakeLauncher`* represents the central functionality of the software. It generates a native Windows launcher for a JLink-generated Java application image. +Firstly, get the latest version of the jlgen executable from the project's [release page](https://github.com/michab66/jlaunch/releases). + +`jlgen.exe` is a command line application that supports different commands. Execute `jlgen` to get a list of the supported commands. + +## How to generate a launcher -- `MakeLauncher` + +The command *`MakeLauncher`* represents the central functionality of the software. It generates a native Windows launcher for a JLink-generated Java application image. The command line is + + `jlgen MakeLauncher `. The required parameters are: * The name of the target file, e.g. 'Farboo.exe'. If the .exe extension is not specified then it is added. @@ -23,11 +32,35 @@ The required parameters are: A complete sample call may look like `jlgen MakeLauncher C:\cygwin64\tmp\Farboo.exe ..\mmt-icon-1024.png app.mmt de.michab.app.mmt.Mmt`. - -## I have the launcher executable, what now? -Note that the generated launcher--in our example 'Farboo.exe'--has to be placed in the existing jlink image directory hierarchy at the same position where the file `jvm.dll` is located. This is currently `{jlink-app-root}/bin/server` but this may change in coming versions of the Jdk. (It is a deliberate decision not to look-up the jvm.dll dynamically to prevent to start the target application using a wrong Jdk/Jre that non-deterministically happened to be found.) -In Windows Explorer the launcher is displayed with the application icon that was passed to `jlgen.exe` above. On a double click your application opens, joy starts :) +## How to generate a Windows `.ico`-file -- `CreateWindowsIcon` + +The command *`CreateWindowsIcon`* lets you create a Windows `.ico`-file from a given input `.png`-file. Internally the input `.png` gets scaled to square images of the pixel sizes 16x16, 32x32, 64x64, 128x128 and 256x256. The resulting `.ico`-file is generated in the directory of the input file, with an `.ico` suffix. The command line is: + + `jlgen CreateWindowsIcon `. + +A sample command line is + + `jlgen CreateWindowsIcon felix.png`. + +This results in the creation of the file `felix.ico` in the same directory as `felix.png`. + +## How to generate a MacOS `.icns`-file -- `CreateAppleIcon` + +The command *`CreateAppleIcon`* lets you create a MacOS `.icns`-file from a given input `.png`-file. Internally the input `.png` gets scaled to square images of the pixel sizes 16x16, 32x32, 64x64, 128x128 and 256x256. The resulting `.icns`-file is generated in the directory of the input file, with an `.icns` suffix. The command line is: + + `jlgen CreateAppleIcon `. + +A sample command line is + + `jlgen CreateAppleIcon felix.png`. + +This results in the creation of the file `felix.icns` in the same directory as `felix.png`. + +# I have the launcher executable and the icon files, what now? +Note that the generated launcher--in our example 'Farboo.exe'--has to be placed in the existing jlink image directory hierarchy at the same position where the file `jvm.dll` is located. This is currently `{jlink-app-root}/bin/server` but this may change in coming versions of the Jdk. + +In Windows Explorer the launcher is displayed with the application icon that was passed to `jlgen.exe` above. On a double click your application opens and joy starts :^) The next step for professional application packing is to create a native Windows installer based on the JLink file system including the `jlgen`-generated launcher that has to be configured as the start application. We use the [WiX toolset](https://wixtoolset.org/) for this purpose, but this is a different story. diff --git a/jlgen/RtIcon.cpp b/jlgen/RtIcon.cpp index d200375..246a251 100644 --- a/jlgen/RtIcon.cpp +++ b/jlgen/RtIcon.cpp @@ -43,4 +43,4 @@ void RtIcon::update(HANDLE resourceHolder, int resourceId) } } // namespace windows -} // namespace micbinz +} // namespace mob diff --git a/jlgen/RtIcon.h b/jlgen/RtIcon.h index 4e89820..200f7c6 100644 --- a/jlgen/RtIcon.h +++ b/jlgen/RtIcon.h @@ -32,6 +32,11 @@ namespace windows void update(HANDLE resourceHolder, int resourceId); + const std::vector& raw_png() + { + return data_; + } + GRPICONDIRENTRY GetDirectoryEntry() { return directoryEntry_; @@ -39,4 +44,4 @@ namespace windows }; } // namespace windows -} // namespace micbinz +} // namespace mob diff --git a/jlgen/jlgen.cpp b/jlgen/jlgen.cpp index 3af8d89..e162570 100644 --- a/jlgen/jlgen.cpp +++ b/jlgen/jlgen.cpp @@ -169,12 +169,49 @@ namespace jlgen return EXIT_SUCCESS; } - int WriteImageSet( - const string& file) + /** + * Create an .ico icon file for Windows. Create target file at position + * of input file with extension .ico. + * + * @param pngFile Input png. + */ + int CreateWindowsIcon( + const string& pngFile) { - smack::util::icons::WriteImageSet( - file, - IMAGE_SIZES ); + path icnFile = pngFile; + icnFile.replace_extension(".ico"); + + cerr << "Writing icon file: " << icnFile << endl; + + smack::util::icons::CreateWindowsIcon( + pngFile, + IMAGE_SIZES, + icnFile); + + return EXIT_SUCCESS; + } + + /** + * Create an .icns icon for Mac. Create target file at position + * of input file with extension .icns. + * + * @param pngFile Input png. + */ + int CreateAppleIcon( + const string& pngFile) + { + path icnFile = pngFile; + icnFile.replace_extension(".icns"); + + cerr << "Writing icon file: " << icnFile << endl; + + // TODO(michab66) The Apple iconviewer expects 16,32,128,256,512. + // But the file with our current setting seems to work. + smack::util::icons::CreateAppleIcon( + pngFile, + IMAGE_SIZES, + icnFile); + return EXIT_SUCCESS; } @@ -199,10 +236,16 @@ namespace jlgen "startClass" }), Commands::make( - "WriteImageSet", - WriteImageSet, + "CreateWindowsIcon", + CreateWindowsIcon, + { + "pngFilename", + }), + Commands::make( + "CreateAppleIcon", + CreateAppleIcon, { - "imageFilename" + "pngFilename", }) ); diff --git a/jlgen/mod_icons.cpp b/jlgen/mod_icons.cpp index c00d2e5..99c0038 100644 --- a/jlgen/mod_icons.cpp +++ b/jlgen/mod_icons.cpp @@ -11,6 +11,8 @@ #include #include +#include + // See https://docs.microsoft.com/en-us/windows/uwp/design/globalizing/use-utf8-code-page #undef UNICODE @@ -207,39 +209,6 @@ namespace smack { namespace util { namespace icons { -/** - * Use the passed image to create a set of scaled, square images in the - * dimensions 16, 32, 64, 128, 256. It is recommended to pass a square - * image though all image sizes will do. - */ -void WriteImageSet( - const path& sourceFile, - const std::initializer_list sizes) -{ - InitGdiPlus init; - - if (!exists(sourceFile)) - throw std::invalid_argument("File not found."); - - std::wstring wideName = - sourceFile.c_str(); - - Gdiplus::Bitmap bitmap{ wideName.c_str() }; - - CLSID fileClsid = - GetClsid(bitmap); - if (IsEqualCLSID(fileClsid, CLSID_NULL)) - throw std::invalid_argument("Unknown file type."); - - string baseName = - GetPath(sourceFile.generic_string()); - string suffix = - GetSuffix(sourceFile.generic_string()); - - for (auto c : sizes) - WriteImageFile(baseName, suffix, c, fileClsid, bitmap); -} - /** * */ @@ -282,6 +251,208 @@ void CreateIcons( } } +/** + * See header. + */ +void CreateWindowsIcon( + const path& sourcePng, + const std::initializer_list sizes, + const path& targetIco) +{ + std::vector> outHolder; + smack::util::icons::CreateIcons( + outHolder, + sizes, + sourcePng); + + std::vector fileContent; + + // Add the prefix icondir structure. + ICONDIR icondir{ + 0, + 1, + static_cast(outHolder.size()) }; + rawAppend( + fileContent, + &icondir, + sizeof(icondir) - sizeof(ICONDIRENTRY)); + + // Add the directory entry structures. + size_t currentOffset{ + fileContent.size() + (outHolder.size() * sizeof(ICONDIRENTRY) ) + }; + + for (const std::unique_ptr& c : outHolder) + { + auto grpDirEntry = + c->GetDirectoryEntry(); + + ICONDIRENTRY dirEntry; + dirEntry.bWidth = + grpDirEntry.bWidth; + dirEntry.bHeight = + grpDirEntry.bHeight; + dirEntry.bColorCount = + grpDirEntry.bColorCount; + dirEntry.bReserved = + grpDirEntry.bReserved; + dirEntry.wPlanes = + grpDirEntry.wPlanes; + dirEntry.wBitCount = + grpDirEntry.wBitCount; + dirEntry.dwBytesInRes = + grpDirEntry.dwBytesInRes; + + dirEntry.dwImageOffset = + static_cast(currentOffset); + currentOffset += c->raw_png().size(); + + rawAppend( + fileContent, + &dirEntry); + } + + // Add the actual entries. + for (const std::unique_ptr& c : outHolder) + { + auto pngData = + c->raw_png(); + rawAppend( + fileContent, + pngData.data(), + pngData.size() + ); + } + + // Write to file. + std::ofstream fout( + targetIco, + std::ios::binary); + void* data = + fileContent.data(); + fout.write( + static_cast(data), + fileContent.size()); + // Trigger write error here, not in destructor. + fout.close(); +} + +/** + * See header. + * + * The implementation uses info from + * https://en.wikipedia.org/wiki/Apple_Icon_Image_format + */ +void CreateAppleIcon( + const path& sourcePng, + const std::initializer_list sizes, + const path& targetIco) +{ + std::vector> outHolder; + smack::util::icons::CreateIcons( + outHolder, + sizes, + sourcePng); + + std::vector fileContent; + + // Write the lead-in magic literal. + rawAppend( + fileContent, + "icns", + 4 ); + + // Compute the size of the file we generate. + { + uint32_t fileSize = static_cast( + // Sizeof magic literal. + fileContent.size() + + sizeof(fileSize) + + // 8 = sizeof( IconType ) + sizeof( LengthOfData ). + (outHolder.size() * 8) ); + for (const std::unique_ptr& c : outHolder) + fileSize += static_cast(c->raw_png().size()); + fileContent.reserve(fileSize); + + // MSVC intrinsic. + fileSize = _byteswap_ulong(fileSize); + + rawAppend( + fileContent, + &fileSize + ); + } + + for (const std::unique_ptr& c : outHolder) + { + auto grpDirEntry = + c->GetDirectoryEntry(); + + const char* OSType; + switch (grpDirEntry.bWidth) + { + case 16: + OSType = "icp4"; + break; + case 32: + OSType = "icp5"; + break; + case 64: + OSType = "icp6"; + break; + case 128: + OSType = "ic07"; + break; + // Zero means actually 256. + case 0: + OSType = "ic08"; + break; + default: + throw std::invalid_argument("Unexpected size."); + } + + // Icon type. + rawAppend( + fileContent, + OSType, + 4 ); + + auto data = + c->raw_png(); + uint32_t size = + static_cast(data.size()); + // Size is 'including type and length'. + size += + (sizeof(size) + 4); + size = + _byteswap_ulong(size); + + // Length of data, big endian. + rawAppend( + fileContent, + &size + ); + // Icon data. + rawAppend( + fileContent, + data.data(), + data.size() + ); + } + + // Write to file. + std::ofstream fout( + targetIco, + std::ios::binary); + void* data = + fileContent.data(); + fout.write( + static_cast(data), + fileContent.size()); + // Trigger write error here, not in destructor. + fout.close(); +} + } } } diff --git a/jlgen/mod_icons.hpp b/jlgen/mod_icons.hpp index a4fca48..d7da385 100644 --- a/jlgen/mod_icons.hpp +++ b/jlgen/mod_icons.hpp @@ -22,16 +22,41 @@ using std::string; using std::experimental::filesystem::path; /** - * Use the passed image to create a set of scaled, square images in the + * Create a Windows .ico file from an input .png file. + * Use the passed sizes to create a set of scaled, square images in the * supported dimensions. It is recommended to pass a square image though * all image sizes will do. + * + * @param sourcePng The source .png file. This must exist. + * @param sizes A list of sizes. Used to create square images. + * @param targetIco The target file. If this exists it gets overwritten. + */ +void CreateWindowsIcon( + const path& sourcePng, + const std::initializer_list sizes, + const path& targetIco); + +/** + * Create an MacOS .icns file from an input .png file. + * Use the passed sizes to create a set of scaled, square images in the + * supported dimensions. It is recommended to pass a square image though + * all image sizes will do. + * + * @param sourcePng The source .png file. This must exist. + * @param sizes A list of sizes. Used to create square images. + * @param targetIco The target file. If this exists it gets overwritten. */ -void WriteImageSet( - const path& sourceFile, - const std::initializer_list sizes); +void CreateAppleIcon( + const path& sourcePng, + const std::initializer_list sizes, + const path& targetIco); /** * Create a set of scaled icons and place them into the passed container. + * + * @param outHolder The target container. + * @param sizes A list of sites. + * @param sourcePng The input .png file. */ void CreateIcons( std::vector>& outHolder, diff --git a/test/6858395_0.png b/test/6858395_0.png new file mode 100644 index 0000000..b79411b Binary files /dev/null and b/test/6858395_0.png differ