From e45e7b256d7ee9869bfac79a552422c4e9383281 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Tue, 21 Aug 2018 17:54:27 -0700 Subject: [PATCH] Introduce mipgen frontend. (#120) * Introduce mipgen frontend. For now this is fairly simple although it does let you choose a filter. Another cool feature is that an HTML file is generated which makes it easy to review the generated miplevels. This does not expose the Boundary enum to users because it has not yet been implemented in the core Image library. * Fix up mipgen per code review. --- CMakeLists.txt | 1 + README.md | 1 + libs/image/include/image/ImageSampler.h | 22 ++ libs/image/src/ImageSampler.cpp | 46 ++++ libs/image/tests/reference/index.html | 47 ++++ libs/image/tests/reference/mip0_200x100.png | Bin 0 -> 357 bytes libs/image/tests/reference/mip0_300x100.png | Bin 0 -> 382 bytes libs/image/tests/reference/mip0_5x10.png | Bin 0 -> 112 bytes libs/image/tests/reference/mip1_200x100.png | Bin 0 -> 434 bytes libs/image/tests/reference/mip1_300x100.png | Bin 0 -> 487 bytes libs/image/tests/reference/mip1_5x10.png | Bin 0 -> 116 bytes libs/image/tests/reference/mip2_200x100.png | Bin 0 -> 309 bytes libs/image/tests/reference/mip2_300x100.png | Bin 0 -> 346 bytes libs/image/tests/reference/mip2_5x10.png | Bin 0 -> 89 bytes libs/image/tests/reference/mip3_200x100.png | Bin 0 -> 165 bytes libs/image/tests/reference/mip3_300x100.png | Bin 0 -> 189 bytes libs/image/tests/reference/mip3_5x10.png | Bin 0 -> 85 bytes libs/image/tests/reference/mip4_200x100.png | Bin 0 -> 135 bytes libs/image/tests/reference/mip4_300x100.png | Bin 0 -> 147 bytes libs/image/tests/reference/mip5_200x100.png | Bin 0 -> 110 bytes libs/image/tests/reference/mip5_300x100.png | Bin 0 -> 119 bytes libs/image/tests/reference/mip6_200x100.png | Bin 0 -> 91 bytes libs/image/tests/reference/mip6_300x100.png | Bin 0 -> 94 bytes libs/image/tests/reference/mip7_200x100.png | Bin 0 -> 85 bytes libs/image/tests/reference/mip7_300x100.png | Bin 0 -> 88 bytes libs/image/tests/reference/mip8_300x100.png | Bin 0 -> 85 bytes libs/image/tests/test_image.cpp | 29 +++ tools/mipgen/CMakeLists.txt | 28 +++ tools/mipgen/src/main.cpp | 255 ++++++++++++++++++++ 29 files changed, 429 insertions(+) create mode 100644 libs/image/tests/reference/index.html create mode 100644 libs/image/tests/reference/mip0_200x100.png create mode 100644 libs/image/tests/reference/mip0_300x100.png create mode 100644 libs/image/tests/reference/mip0_5x10.png create mode 100644 libs/image/tests/reference/mip1_200x100.png create mode 100644 libs/image/tests/reference/mip1_300x100.png create mode 100644 libs/image/tests/reference/mip1_5x10.png create mode 100644 libs/image/tests/reference/mip2_200x100.png create mode 100644 libs/image/tests/reference/mip2_300x100.png create mode 100644 libs/image/tests/reference/mip2_5x10.png create mode 100644 libs/image/tests/reference/mip3_200x100.png create mode 100644 libs/image/tests/reference/mip3_300x100.png create mode 100644 libs/image/tests/reference/mip3_5x10.png create mode 100644 libs/image/tests/reference/mip4_200x100.png create mode 100644 libs/image/tests/reference/mip4_300x100.png create mode 100644 libs/image/tests/reference/mip5_200x100.png create mode 100644 libs/image/tests/reference/mip5_300x100.png create mode 100644 libs/image/tests/reference/mip6_200x100.png create mode 100644 libs/image/tests/reference/mip6_300x100.png create mode 100644 libs/image/tests/reference/mip7_200x100.png create mode 100644 libs/image/tests/reference/mip7_300x100.png create mode 100644 libs/image/tests/reference/mip8_300x100.png create mode 100644 tools/mipgen/CMakeLists.txt create mode 100644 tools/mipgen/src/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d3f55771e0..9457beda7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,6 +288,7 @@ if (NOT ANDROID) add_subdirectory(${TOOLS}/filamesh) add_subdirectory(${TOOLS}/matc) add_subdirectory(${TOOLS}/matinfo) + add_subdirectory(${TOOLS}/mipgen) add_subdirectory(${TOOLS}/normal-blending) add_subdirectory(${TOOLS}/roughness-prefilter) add_subdirectory(${TOOLS}/skygen) diff --git a/README.md b/README.md index 1febd4cf19..f93ca51c5f 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Many other features have been either prototyped or planned: - `filamesh`: Mesh converter - `matc`: Material compiler - `matinfo` Displays information about materials compiled with `matc` + - `mipgen` Generates a series of miplevels from a source image. - `normal-blending`: Tool to blend normal maps - `roughness-prefilter`: Pre-filters a roughness map from a normal map to reduce aliasing - `skygen`: Physically-based sky environment texture generator diff --git a/libs/image/include/image/ImageSampler.h b/libs/image/include/image/ImageSampler.h index 7fc2f5251d..f09eff9640 100644 --- a/libs/image/include/image/ImageSampler.h +++ b/libs/image/include/image/ImageSampler.h @@ -129,6 +129,28 @@ LinearImage resampleImage(const LinearImage& source, uint32_t width, uint32_t he void computeSingleSample(const LinearImage& source, float x, float y, SingleSample* result, Filter filter = Filter::BOX); +/** + * Generates a sequence of miplevels using the requested filter. To determine the number of mips + * it would take to get down to 1x1, see getMipmapCount. + * + * Source image need not be power-of-two. In the result vector, the half-size image is returned at + * index 0, the quarter-size image is at index 1, etc. Please note that the original-sized image is + * not included. + */ +void generateMipmaps(const LinearImage& source, Filter, LinearImage* result, uint32_t mipCount); + +/** + * Returns the number of miplevels it would take to downsample the given image down to 1x1. This + * number does not include the original image (i.e. mip 0). + */ +uint32_t getMipmapCount(const LinearImage& source); + +/** + * Given the string name of a filter, converts it to uppercase and returns the corresponding + * enum value. If no corresponding enumerant exists, returns DEFAULT. + */ +Filter filterFromString(const char* name); + } // namespace image #endif /* IMAGE_IMAGESAMPLER_H */ diff --git a/libs/image/src/ImageSampler.cpp b/libs/image/src/ImageSampler.cpp index 52258c92a1..ab6fd4ebba 100644 --- a/libs/image/src/ImageSampler.cpp +++ b/libs/image/src/ImageSampler.cpp @@ -19,9 +19,11 @@ #include #include +#include #include #include +#include using namespace image; @@ -320,4 +322,48 @@ void computeSingleSample(const LinearImage& source, float x, float y, SingleSamp } } +// Unlike traditional mipmap generation, our implementation generates all levels from the original +// image, under the premise that this produces a higher quality result. +void generateMipmaps(const LinearImage& source, Filter filter, LinearImage* result, uint32_t mips) { + mips = std::min(mips, getMipmapCount(source)); + uint32_t width = source.getWidth(); + uint32_t height = source.getHeight(); + for (uint32_t n = 0; n < mips; ++n) { + width = std::max(width >> 1, 1u); + height = std::max(height >> 1, 1u); + result[n] = resampleImage(source, width, height, filter); + } +} + +uint32_t getMipmapCount(const LinearImage& source) { + uint32_t width = source.getWidth(); + uint32_t height = source.getHeight(); + uint32_t count = 0; + while (width > 1 || height > 1) { + ++count; + width = std::max(width >> 1, 1u); + height = std::max(height >> 1, 1u); + } + return count; +} + +Filter filterFromString(const char* rawname) { + using namespace utils; + using namespace std; + static const unordered_map map = { + { "BOX", Filter::BOX}, + { "NEAREST", Filter::NEAREST}, + { "HERMITE", Filter::HERMITE}, + { "GAUSSIAN", Filter::GAUSSIAN_SCALARS}, + { "NORMALS", Filter::GAUSSIAN_NORMALS}, + { "MITCHELL", Filter::MITCHELL}, + { "LANCZOS", Filter::LANCZOS}, + { "MINIMUM", Filter::MINIMUM}, + }; + string name = rawname; + for (auto& c: name) c = toupper((unsigned char) c); + auto iter = map.find({ name.c_str(), name.size() }); + return iter == map.end() ? Filter::DEFAULT : iter->second; +} + } // namespace image diff --git a/libs/image/tests/reference/index.html b/libs/image/tests/reference/index.html new file mode 100644 index 0000000000..13eab9671a --- /dev/null +++ b/libs/image/tests/reference/index.html @@ -0,0 +1,47 @@ + + + +test_image + + + + +

+Modern CSS lets you specify nearest-neighbor scaling, so a nice way of examining the results from +our filtering tests is this tiny web page. It can be refreshed at the touch of a button. Moreover +Chrome respects the color space attribute of PNG images, which cannot be said of many image viewers. +You can also use the ColorZilla +extension to scrub over color values. +

+ +

+ + + + +

+ +

+ + + + + + + + +

+ + + diff --git a/libs/image/tests/reference/mip0_200x100.png b/libs/image/tests/reference/mip0_200x100.png new file mode 100644 index 0000000000000000000000000000000000000000..cb8c1f80edf0a8c4427c850d50bc854be4be142d GIT binary patch literal 357 zcmeAS@N?(olHy`uVBq!ia0vp^CxAGGg9%9bJb4iVq*&4&eH|GX+ZGtUm=@2#z$oMC z;uumf=k29~oD2#K%!Z}^=a#bCw=0}vS$gMAbM)oN>(^d0{_%`ZX>oNLVZh2L)78&qol`;+03D8Qe*gdg literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/mip0_300x100.png b/libs/image/tests/reference/mip0_300x100.png new file mode 100644 index 0000000000000000000000000000000000000000..ad84abdadd65cb0b487fe78f729a1f30866221c8 GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0y~yVAKJ!Q#hD_D4bDVB6cUq=SUwgrYSro{vK>Ygr+ zAsjQ4QxblhZ{Ygp%<}JW$A2Ckp8peB0-s1;{9EW?3X7(8A5 KT-G@yGywqWqaeEg literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/mip1_200x100.png b/libs/image/tests/reference/mip1_200x100.png new file mode 100644 index 0000000000000000000000000000000000000000..f34ff8f4fcc35ed83942b7dbd1493b0ce74fec74 GIT binary patch literal 434 zcmeAS@N?(olHy`uVBq!ia0vp^DL`z*!2~2#!(YAxQY`6?zK#rxZ3_%vOp9k=V9fM% zaSX|5d^_`WA+w@@i{nng_+2ajSCsNRSSGRPEjp;B;gQxCt-Q< zT!|A)jc+bEC!NHvdB5|G4x^a&gAa$Aeif_l`?uWJ)^~f&JC~x<>#kXpAL@Rv^YF~6 z-SMXH&aDdO(RnKW+i%6ry_*l^&52}k`OsvM9_DCcyLkui+Y+FF&NQG^4xju5)Jp_F z_JVC!=;LMGuu$RLIiUwHSAH(^n(Vt%~$=~dAE9c4PL!LJynN?8vNL1OjIS!}``Lj8vgmhw1{%dF%k?OncYETs$+rvmb0P$ex9Q#Z5TCG-g~N;W aqy0=jxn|!R6+3{Dz~JfX=d#Wzp$Py(@4(*x literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/mip1_300x100.png b/libs/image/tests/reference/mip1_300x100.png new file mode 100644 index 0000000000000000000000000000000000000000..e8aa4c4eb86da37d6d4a0ac5aa645136ebc9ea6c GIT binary patch literal 487 zcmVEv0sloZk>;?ojf+1tBbIykc79kk4 zh=+)K_+<|vKE6L$Lwx+0CepgkbXsHt_M%9F&)4w&5x(B8?nkhO+0sl*zTsfRW0CM= zVJ7+DW+ptEh0la@7M59CM1|@@(etwX9VHxj668fn}H)0Bp+O6p**aG zf|y7}n!%B3fFr#bHeOg|AgWcZLcDOa)iaod@(}XvYGk4GZ!NbPJHcdRgR6;_6A@&#Rl|7!76M@X0ssI2{5I+n00004XF*Lt00D-e zG3b_G0002&Nkl3n^Q!M?dSa2K)`&l+Mst|P^JnRuWb<_2n)Z^yEM*!R1Tf^;ttfyYe<68bZH+|V zi3eUIv9b0Ei5dhjB&p=ENz&Z5BmGDYNqSWO!VkjQ z;06IPBrQS)*9w>xRpAVY)Qb>E&nNIIS?^m(C7*P+l75LNDcQB}&mnHa00000NkvXX Hu0mjfk-&tq literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/mip2_300x100.png b/libs/image/tests/reference/mip2_300x100.png new file mode 100644 index 0000000000000000000000000000000000000000..34c7fc381a3e22ef178395d5e116afc9c5ae2a3f GIT binary patch literal 346 zcmV-g0j2(lP)000>X0ssI2_G&~y00004XF*Lt00D-e zG3b_G0003INkl z9xO{&=>G|8WHV!UVL`*Bw-~P_i!;fIRNa5P>my z4P|+~X%QwgagjVV4~hDDrq~hRH9UU}FW+gJzM?Oii1v>zbv4hJ2)u)zy0rgJQ=7+; zt>=|P&R+)#RXQU(cWn_(D^N9^F{EzXwe`RAzQEfeZ}*XxB1q73yhvL%?`*3Px^%`D soo45E8j|?_P7^Y(&}sUwVgJ4K4QoZrGolQdxBvhE07*qoM6N<$g3Y0fl>h($ literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/mip2_5x10.png b/libs/image/tests/reference/mip2_5x10.png new file mode 100644 index 0000000000000000000000000000000000000000..abec14345d60892dd43f0c90211c34df948b403f GIT binary patch literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^j6lr9!2~459xE0ADVB6cUq=SUwgrYSro{vK0-i38 mAsjQ4H$0qs`pAI;3=FKo3@jVWY*UH71gU6 zjcdL((|jqpu-#J|3j;{EZG ndFJFK<1-zTv>f8E{GZRy@y`9BUQg6Ap#2P827PiLEVY+9=D iZN6uX_Uae$d~>$)^8Jk$lbsJViNVv=&t;ucLK6TQj4ZGK literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/mip4_300x100.png b/libs/image/tests/reference/mip4_300x100.png new file mode 100644 index 0000000000000000000000000000000000000000..c9f9f891e9b4d356fff377480e89dca993477c7b GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&M!2~27Uq|)=DVB6cUq=SUwgrYSro{vKUY;(F zArezt`y9C#3^|yWZ>oRsKmL0Bk-Z@SnVEcw!Xixv4mv)xlW#qRqIXexuJtDnm{r-UW|Pp&m7 literal 0 HcmV?d00001 diff --git a/libs/image/tests/reference/mip5_200x100.png b/libs/image/tests/reference/mip5_200x100.png new file mode 100644 index 0000000000000000000000000000000000000000..e750aa19d4aa59a41500da9153a4cb1488dbb610 GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^Y(UJ+!2~4jlV7d@QY`6?zK#rxZ3_%vOp6EdRXtrC zLpWyoo-kx&FyLU`(7^d;^1B{E@TdZ|Dbx=lFz q;h35Hp00i_>zopr0NQ^QDF6Tf literal 0 HcmV?d00001 diff --git a/libs/image/tests/test_image.cpp b/libs/image/tests/test_image.cpp index 50ad9a81db..6b8279bc17 100644 --- a/libs/image/tests/test_image.cpp +++ b/libs/image/tests/test_image.cpp @@ -278,6 +278,35 @@ TEST_F(ImageTest, ColorTransformRGBA) { // NOLINT ASSERT_NEAR(pixels[3].w, 0.99183642f, 0.001f); } +TEST_F(ImageTest, Mipmaps) { // NOLINT + Filter filter = filterFromString("HERMITE"); + ASSERT_EQ(filter, Filter::HERMITE); + + // Miplevels: 5x10, 2x5, 1x2, 1x1. + LinearImage src = createColorFromAscii( + "44444 41014 40704 41014 44444 44444 41014 40704 41014 44444"); + uint32_t count = getMipmapCount(src); + ASSERT_EQ(count, 3); + std::vector mips(count); + generateMipmaps(src, filter, mips.data(), count); + updateOrCompare(src, "mip0_5x10.png"); + for (uint32_t index = 0; index < count; ++index) { + updateOrCompare(mips[index], "mip" + std::to_string(index + 1) + "_5x10.png"); + } + + // Test color space with a classic RED => GREEN color gradient. + src = createColorFromAscii("12"); + src = resampleImage(src, 200, 100, Filter::NEAREST); + count = getMipmapCount(src); + ASSERT_EQ(count, 7); + mips.resize(count); + generateMipmaps(src, filter, mips.data(), count); + updateOrCompare(src, "mip0_200x100.png"); + for (uint32_t index = 0; index < count; ++index) { + updateOrCompare(mips[index], "mip" + std::to_string(index + 1) + "_200x100.png"); + } +} + static void printUsage(const char* name) { string exec_name(utils::Path(name).getName()); string usage( diff --git a/tools/mipgen/CMakeLists.txt b/tools/mipgen/CMakeLists.txt new file mode 100644 index 0000000000..999d61fec8 --- /dev/null +++ b/tools/mipgen/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.1) +project(mipgen) + +set(TARGET mipgen) + +# ================================================================================================== +# Source files +# ================================================================================================== +set(SRCS src/main.cpp) + +# ================================================================================================== +# Target definitions +# ================================================================================================== +add_executable(${TARGET} ${SRCS}) +target_link_libraries(${TARGET} PRIVATE math utils z image imageio getopt) + +# ================================================================================================= +# Licenses +# ================================================================================================== +set(MODULE_LICENSES getopt libpng tinyexr libz) +set(GENERATION_ROOT ${CMAKE_CURRENT_BINARY_DIR}/generated) +list_licenses(${GENERATION_ROOT}/licenses/licenses.inc ${MODULE_LICENSES}) +target_include_directories(${TARGET} PRIVATE ${GENERATION_ROOT}) + +# ================================================================================================== +# Installation +# ================================================================================================== +install(TARGETS ${TARGET} RUNTIME DESTINATION bin) diff --git a/tools/mipgen/src/main.cpp b/tools/mipgen/src/main.cpp new file mode 100644 index 0000000000..4664c000cb --- /dev/null +++ b/tools/mipgen/src/main.cpp @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +#include + +#include + +#include +#include + +using namespace image; +using namespace std; +using namespace utils; + +static ImageEncoder::Format g_format = ImageEncoder::Format::PNG_LINEAR; +static bool g_formatSpecified = false; +static bool g_createGallery = false; +static string g_compression = ""; +static Filter g_filter = Filter::DEFAULT; + +static const char* USAGE = R"TXT( +MIPGEN generates mipmaps for an image down to the 1x1 level. + +Output filenames are generated using the specified printf pattern. +For example, "mip%2d.png" would generate mip01.png, mip02.png, etc. +Note that miplevel 0 is not generated since it is the original image. + +Usage: + MIPGEN [options] + +Options: + --help, -h + print this message + --license + print copyright and license information + --gallery, -g + generate HTML gallery for review purposes (mipmap.html) + --format=[exr|hdr|rgbm|psd|png|dds], -f [exr|hdr|rgbm|psd|png|dds] + specify output file format, inferred from output pattern if omitted + --kernel=[box|nearest|hermite|gaussian|normals|mitchell|lanczos|min], -k [filter] + specify filter kernel type (defaults to LANCZOS) + --compression=COMPRESSION, -c COMPRESSION + format specific compression: + PNG: Ignored + Radiance: Ignored + Photoshop: 16 (default), 32 + OpenEXR: RAW, RLE, ZIPS, ZIP, PIZ (default) + DDS: 8, 16 (default), 32 + +Example: + MIPGEN -g --kernel=hermite grassland.png mip_%03d.png +)TXT"; + +static const char* HTML_PREFIX = R"HTML( + + + + + +)HTML"; + +static const char* HTML_SUFFIX = R"HTML( + +)HTML"; + +static void printUsage(const char* name) { + string execName(Path(name).getName()); + const string from("MIPGEN"); + string usage(USAGE); + for (size_t pos = usage.find(from); pos != string::npos; pos = usage.find(from, pos)) { + usage.replace(pos, from.length(), execName); + } + puts(usage.c_str()); +} + +static void license() { + cout << + #include "licenses/licenses.inc" + ; +} + +static int handleArguments(int argc, char* argv[]) { + static constexpr const char* OPTSTR = "hlgf:c:k:"; + static const struct option OPTIONS[] = { + { "help", no_argument, 0, 'h' }, + { "license", no_argument, 0, 'l' }, + { "gallery", no_argument, 0, 'g' }, + { "format", required_argument, 0, 'f' }, + { "compression", required_argument, 0, 'c' }, + { "kernel", required_argument, 0, 'k' }, + { 0, 0, 0, 0 } // termination of the option list + }; + + int opt; + int optionIndex = 0; + + while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &optionIndex)) >= 0) { + string arg(optarg ? optarg : ""); + switch (opt) { + default: + case 'h': + printUsage(argv[0]); + exit(0); + case 'l': + license(); + exit(0); + case 'g': + g_createGallery = true; + break; + case 'k': { + bool isvalid; + g_filter = filterFromString(arg.c_str()); + if (g_filter == Filter::DEFAULT) { + cerr << "Warning: unrecognized filter, falling back to DEFAULT." << endl; + } + break; + } + case 'f': + if (arg == "png") { + g_format = ImageEncoder::Format::PNG; + g_formatSpecified = true; + } + if (arg == "hdr") { + g_format = ImageEncoder::Format::HDR; + g_formatSpecified = true; + } + if (arg == "rgbm") { + g_format = ImageEncoder::Format::RGBM; + g_formatSpecified = true; + } + if (arg == "exr") { + g_format = ImageEncoder::Format::EXR; + g_formatSpecified = true; + } + if (arg == "psd") { + g_format = ImageEncoder::Format::PSD; + g_formatSpecified = true; + } + if (arg == "dds") { + g_format = ImageEncoder::Format::DDS_LINEAR; + g_formatSpecified = true; + } + break; + case 'c': + g_compression = arg; + break; + } + } + + return optind; +} + +int main(int argc, char* argv[]) { + int optionIndex = handleArguments(argc, argv); + int numArgs = argc - optionIndex; + if (numArgs < 2) { + printUsage(argv[0]); + return 1; + } + Path inputPath(argv[optionIndex++]); + string outputPattern(argv[optionIndex]); + if (!g_formatSpecified) { + constexpr bool forceLinear = true; + g_format = ImageEncoder::chooseFormat(outputPattern, forceLinear); + } + + puts("Reading image..."); + ifstream inputStream(inputPath.getPath(), ios::binary); + LinearImage sourceImage = ImageDecoder::decode(inputStream, inputPath.getPath()); + if (!sourceImage.isValid()) { + cerr << "Unable to open image: " << inputPath.getPath() << endl; + exit(1); + } + + puts("Generating miplevels..."); + uint32_t count = getMipmapCount(sourceImage); + vector miplevels(count); + generateMipmaps(sourceImage, g_filter, miplevels.data(), count); + + puts("Writing image files to disk..."); + char path[256]; + uint32_t mip = 1; // start at 1 because 0 is the original image + for (auto image: miplevels) { + int result = snprintf(path, sizeof(path), outputPattern.c_str(), mip++); + if (result < 0 || result >= sizeof(path)) { + cerr << "Output pattern is too long." << endl; + exit(1); + } + ofstream outputStream(path, ios::binary | ios::trunc); + if (!outputStream) { + cerr << "The output file cannot be opened: " << path << endl; + } else { + ImageEncoder::encode(outputStream, g_format, image, g_compression, path); + outputStream.close(); + if (!outputStream) { + cerr << "An error occurred while writing the output file: " << path << endl; + } + } + } + + if (g_createGallery) { + puts("Generating mipmaps.html..."); + char tag[256]; + mip = 1; + const char* pattern = R"()"; + const uint32_t width = sourceImage.getWidth(); + const uint32_t height = sourceImage.getHeight(); + ofstream html("mipmaps.html", ios::trunc); + html << HTML_PREFIX; + int result = snprintf(tag, sizeof(tag), pattern, inputPath.c_str(), width, height); + if (result < 0 || result >= sizeof(tag)) { + cerr << "Output pattern is too long." << endl; + exit(1); + } + html << tag << std::endl; + for (auto image: miplevels) { + snprintf(path, sizeof(path), outputPattern.c_str(), mip++); + result = snprintf(tag, sizeof(tag), pattern, path, width, height); + if (result < 0 || result >= sizeof(tag)) { + cerr << "Output pattern is too long." << endl; + exit(1); + } + html << tag << std::endl; + } + html << HTML_SUFFIX; + } + + puts("Done."); +}