1 module dlibwebp; 2 3 private { 4 import dlib.image; 5 import webp.encode; 6 import webp.decode; 7 import dlib.core.compound; 8 import dlib.core.stream; 9 import dlib.filesystem.local; 10 import core.memory : GC; 11 import std.array; 12 } 13 14 class WEBPLoadException: ImageLoadException { 15 this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { 16 super(msg, file, line, next); 17 } 18 } 19 20 SuperImage loadWEBP(string filename) { 21 InputStream input = openForInput(filename); 22 auto img = loadWEBP(input); 23 input.close(); 24 return img; 25 } 26 27 SuperImage loadWEBP(InputStream input) { 28 auto fileContent = appender!(ubyte[])(); 29 ubyte[0x1000] buffer; 30 while (input.readable) { 31 size_t count = input.readBytes(buffer.ptr, buffer.length); 32 if (count == 0) { 33 break; 34 } 35 for (int i = 0; i < count; i++) { 36 fileContent.put(buffer[i]); 37 } 38 } 39 return loadWEBP(fileContent.data); 40 } 41 42 SuperImage loadWEBP(in ubyte[] webp) { 43 int width; 44 int height; 45 ubyte* argbPointer = WebPDecodeRGBA(webp.ptr, webp.length, &width, &height); 46 GC.addRange(argbPointer, (width * height * 4)); 47 ubyte[] argbArray = argbPointer[0 .. (width * height * 4)]; 48 49 SuperImage rgbaImage = defaultImageFactory.createImage(width, height, 4, 8); 50 foreach(i, v; argbArray) { 51 rgbaImage.data[i] = v; 52 } 53 return rgbaImage; 54 } 55 56 57 void saveWEBP(SuperImage img, int quality, string filename) { 58 OutputStream output = openForOutput(filename); 59 Compound!(bool, string) res = saveWEBP(img, quality, output); 60 output.close(); 61 62 if (!res[0]) { 63 throw new WEBPLoadException(res[1]); 64 } 65 } 66 void saveLosslessWEBP(SuperImage img, string filename) { 67 OutputStream output = openForOutput(filename); 68 Compound!(bool, string) res = saveLosslessWEBP(img, output); 69 output.close(); 70 71 if (!res[0]) { 72 throw new WEBPLoadException(res[1]); 73 } 74 } 75 76 Compound!(bool, string) saveWEBP(SuperImage img, int quality, OutputStream output) { 77 ubyte[] result = saveWEBPToArray(img, quality); 78 output.writeArray(result); 79 return compound(true, ""); 80 } 81 Compound!(bool, string) saveLosslessWEBP(SuperImage img, OutputStream output) { 82 ubyte[] result = saveLosslessWEBPToArray(img); 83 output.writeArray(result); 84 return compound(true, ""); 85 } 86 87 ubyte[] saveWEBPToArray(SuperImage img, int quality) { 88 if (PixelFormat.L8 == img.pixelFormat || 89 PixelFormat.RGB8 == img.pixelFormat || 90 PixelFormat.L16 == img.pixelFormat || 91 PixelFormat.RGB16 == img.pixelFormat) { 92 return saveLossy(img, quality); 93 } else { 94 return saveLossyWithAlpha(img, quality); 95 } 96 } 97 ubyte[] saveLosslessWEBPToArray(SuperImage img) { 98 if (PixelFormat.L8 == img.pixelFormat || 99 PixelFormat.RGB8 == img.pixelFormat || 100 PixelFormat.L16 == img.pixelFormat || 101 PixelFormat.RGB16 == img.pixelFormat) { 102 return saveLossless(img); 103 } else { 104 return saveLosslessWithAlpha(img); 105 } 106 } 107 108 109 private ubyte[] saveLossyWithAlpha(SuperImage img, int quality) { 110 SuperImage inputImage = img; 111 if (PixelFormat.RGBA8 != img.pixelFormat) { 112 inputImage = convert!(Image!(PixelFormat.RGBA8))(img); 113 } 114 ubyte* outputPointer; 115 size_t outputSize = WebPEncodeRGBA( 116 inputImage.data.ptr, 117 img.width(), 118 img.height(), 119 img.width() * 4, 120 quality, 121 &outputPointer); 122 GC.addRange(outputPointer, outputSize); 123 return outputPointer[0 .. outputSize]; 124 } 125 private ubyte[] saveLossy(SuperImage img, int quality) { 126 SuperImage inputImage = img; 127 if (PixelFormat.RGB8 != img.pixelFormat) { 128 inputImage = convert!(Image!(PixelFormat.RGB8))(img); 129 } 130 ubyte* outputPointer; 131 size_t outputSize = WebPEncodeRGB( 132 inputImage.data.ptr, 133 img.width(), 134 img.height(), 135 img.width() * 3, 136 quality, 137 &outputPointer); 138 GC.addRange(outputPointer, outputSize); 139 return outputPointer[0 .. outputSize]; 140 } 141 142 private ubyte[] saveLosslessWithAlpha(SuperImage img) { 143 SuperImage inputImage = img; 144 if (PixelFormat.RGBA8 != img.pixelFormat) { 145 inputImage = convert!(Image!(PixelFormat.RGBA8))(img); 146 } 147 ubyte* outputPointer; 148 size_t outputSize = WebPEncodeLosslessRGBA( 149 inputImage.data.ptr, 150 img.width(), 151 img.height(), 152 img.width() * 4, 153 &outputPointer); 154 GC.addRange(outputPointer, outputSize); 155 return outputPointer[0 .. outputSize]; 156 } 157 private ubyte[] saveLossless(SuperImage img) { 158 SuperImage inputImage = img; 159 if (PixelFormat.RGB8 != img.pixelFormat) { 160 inputImage = convert!(Image!(PixelFormat.RGB8))(img); 161 } 162 ubyte* outputPointer; 163 size_t outputSize = WebPEncodeLosslessRGB( 164 inputImage.data.ptr, 165 img.width(), 166 img.height(), 167 img.width() * 3, 168 &outputPointer); 169 GC.addRange(outputPointer, outputSize); 170 return outputPointer[0 .. outputSize]; 171 } 172 173 /* 174 private void saveIt(SuperImage input, string filename) { 175 OutputStream outputStream = openForOutput(filename); 176 Compound!(bool, string) res = saveWEBP(cast(SuperImage)input, 85, outputStream); 177 outputStream.close(); 178 assert(res[0]); 179 } 180 */ 181 182 183 184 /** 185 * Run the tests like this: 186 * dub test --debug=featureTest 187 * 188 */ 189 debug (featureTest) { 190 import feature_test; 191 import randomdlibimage; 192 import std.math; 193 194 private SuperImage createImageWithColour(PixelFormat format)(int w, int h, Color4f c) { 195 SuperImage img = new Image!(format)(w, h); 196 foreach(int x; 0..img.width) { 197 foreach(int y; 0..img.height) { 198 img[x, y] = c; 199 } 200 } 201 return img; 202 } 203 private void colorTestLossless(PixelFormat format)(in string fn, Color4f c) { 204 { 205 SuperImage redNonTransparent = createImageWithColour!(format)(500, 400, c); 206 redNonTransparent.saveLosslessWEBP(fn); 207 } 208 SuperImage result = loadWEBP(fn); 209 foreach(int x; 0..result.width) { 210 foreach(int y; 0..result.height) { 211 // WebP supports maximum 8-bit per channel. 212 Color4 expected = c.convert(8); 213 Color4 actual = result[x, y].convert(8); 214 expected.r.shouldEqual(actual.r); 215 expected.g.shouldEqual(actual.g); 216 expected.b.shouldEqual(actual.b); 217 expected.a.shouldEqual(actual.a); 218 } 219 } 220 } 221 222 private void assertTheSame8bitWithAlpha(SuperImage source, SuperImage result) { 223 foreach(int x; 0..result.width) { 224 foreach(int y; 0..result.height) { 225 Color4 expected = source[x, y].convert(8); 226 Color4 actual = result[x, y].convert(8); 227 expected.r.shouldEqual(actual.r); 228 expected.g.shouldEqual(actual.g); 229 expected.b.shouldEqual(actual.b); 230 expected.a.shouldEqual(actual.a); 231 } 232 } 233 } 234 235 unittest { 236 237 feature("Filesystem i/o RGBA8. Lossless.", (f) { 238 f.scenario("Red 1.0", { 239 colorTestLossless!(PixelFormat.RGBA8)( 240 "lossless_RGBA8_red.webp", 241 Color4f(1f, 0f, 0f, 1f) 242 ); 243 }); 244 f.scenario("Red 0.5", { 245 colorTestLossless!(PixelFormat.RGBA8)( 246 "lossless_RGBA8_red_0.5.webp", 247 Color4f(0.5f, 0f, 0f, 1f) 248 ); 249 }); 250 f.scenario("Red 0.01", { 251 colorTestLossless!(PixelFormat.RGBA8)( 252 "lossless_RGBA8_red_0.01.webp", 253 Color4f(0.01f, 0f, 0f, 1f) 254 ); 255 }); 256 f.scenario("Green 1.0", { 257 colorTestLossless!(PixelFormat.RGBA8)( 258 "lossless_RGBA8_green.webp", 259 Color4f(0f, 1f, 0f, 1f) 260 ); 261 }); 262 f.scenario("Blue 1.0", { 263 colorTestLossless!(PixelFormat.RGBA8)( 264 "lossless_RGBA8_blue.webp", 265 Color4f(0f, 0f, 1f, 1f) 266 ); 267 }); 268 f.scenario("Blue 1.0. Opacity 0.8.", { 269 colorTestLossless!(PixelFormat.RGBA8)( 270 "lossless_RGBA8_blue_alpha_0.8.webp", 271 Color4f(0f, 0f, 1f, 0.8f) 272 ); 273 }); 274 f.scenario("Random.", { 275 const fn = "lossless_RGBA8_random.webp"; 276 277 SuperImage img = RandomImages.circles!(PixelFormat.RGBA8)(500, 400); 278 // Alpha pixel. 279 img[0, 0] = Color4f( 280 img[0, 0].r, 281 img[0, 0].g, 282 img[0, 0].b, 283 0.8f); 284 img.saveLosslessWEBP(fn); 285 286 SuperImage result = loadWEBP(fn); 287 abs(result[0, 0].a - 0.8f).shouldBeLessThan(0.02f); // Alpha pixel! 288 abs(result[1, 0].a - 1.0f).shouldBeLessThan(0.01f); 289 assertTheSame8bitWithAlpha(img, result); 290 }); 291 }); 292 293 294 feature("Filesystem i/o RGBA16. Lossless.", (f) { 295 f.scenario("Red 1.0", { 296 colorTestLossless!(PixelFormat.RGBA16)( 297 "lossless_RGBA16_red.webp", 298 Color4f(1f, 0f, 0f, 1f) 299 ); 300 }); 301 f.scenario("Red 0.5", { 302 colorTestLossless!(PixelFormat.RGBA16)( 303 "lossless_RGBA16_red_0.5.webp", 304 Color4f(0.5f, 0f, 0f, 1f) 305 ); 306 }); 307 f.scenario("Red 0.01", { 308 colorTestLossless!(PixelFormat.RGBA16)( 309 "lossless_RGBA16_red_0.01.webp", 310 Color4f(0.01f, 0f, 0f, 1f) 311 ); 312 }); 313 f.scenario("Green 1.0", { 314 colorTestLossless!(PixelFormat.RGBA16)( 315 "lossless_RGBA16_green.webp", 316 Color4f(0f, 1f, 0f, 1f) 317 ); 318 }); 319 f.scenario("Blue 1.0", { 320 colorTestLossless!(PixelFormat.RGBA16)( 321 "lossless_RGBA16_blue.webp", 322 Color4f(0f, 0f, 1f, 1f) 323 ); 324 }); 325 f.scenario("Blue 1.0. Opacity 0.8.", { 326 colorTestLossless!(PixelFormat.RGBA16)( 327 "lossless_RGBA16_blue_alpha_0.8.webp", 328 Color4f(0f, 0f, 1f, 0.8f) 329 ); 330 }); 331 f.scenario("Random.", { 332 const fn = "lossless_RGBA16_random.webp"; 333 334 SuperImage img = RandomImages.circles!(PixelFormat.RGBA16)(500, 400); 335 // Alpha pixel. 336 img[0, 0] = Color4f( 337 img[0, 0].r, 338 img[0, 0].g, 339 img[0, 0].b, 340 0.8f); 341 img.saveLosslessWEBP(fn); 342 343 SuperImage result = loadWEBP(fn); 344 abs(result[0, 0].a - 0.8f).shouldBeLessThan(0.02f); // Alpha pixel! 345 abs(result[1, 0].a - 1.0f).shouldBeLessThan(0.01f); 346 assertTheSame8bitWithAlpha(img, result); 347 }); 348 }); 349 350 feature("Filesystem i/o RGB-8. Lossless.", (f) { 351 f.scenario("Red 1.0", { 352 colorTestLossless!(PixelFormat.RGB8)( 353 "lossless_RGB8_red.webp", 354 Color4f(1f, 0f, 0f, 1f) 355 ); 356 }); 357 f.scenario("Red 0.5", { 358 colorTestLossless!(PixelFormat.RGB8)( 359 "lossless_RGB8_red_0.5.webp", 360 Color4f(0.5f, 0f, 0f, 1f) 361 ); 362 }); 363 f.scenario("Red 0.01", { 364 colorTestLossless!(PixelFormat.RGB8)( 365 "lossless_RGB8_red_0.01.webp", 366 Color4f(0.01f, 0f, 0f, 1f) 367 ); 368 }); 369 f.scenario("Green 1.0", { 370 colorTestLossless!(PixelFormat.RGB8)( 371 "lossless_RGB8_green.webp", 372 Color4f(0f, 1f, 0f, 1f) 373 ); 374 }); 375 f.scenario("Blue 1.0", { 376 colorTestLossless!(PixelFormat.RGB8)( 377 "lossless_RGB8_blue.webp", 378 Color4f(0f, 0f, 1f, 1f) 379 ); 380 }); 381 f.scenario("Random.", { 382 const fn = "lossless_RGB8_random.webp"; 383 SuperImage source = RandomImages.circles!(PixelFormat.RGB8)(500, 400); 384 source.saveLosslessWEBP(fn); 385 SuperImage result = loadWEBP(fn); 386 assertTheSame8bitWithAlpha(source, result); 387 }); 388 }); 389 390 391 feature("Filesystem i/o RGB-16. Lossless.", (f) { 392 f.scenario("Red 1.0", { 393 colorTestLossless!(PixelFormat.RGB16)( 394 "lossless_RGB16_red.webp", 395 Color4f(1f, 0f, 0f, 1f) 396 ); 397 }); 398 f.scenario("Red 0.5", { 399 colorTestLossless!(PixelFormat.RGB16)( 400 "lossless_RGB16_red_0.5.webp", 401 Color4f(0.5f, 0f, 0f, 1f) 402 ); 403 }); 404 f.scenario("Red 0.01", { 405 colorTestLossless!(PixelFormat.RGB16)( 406 "lossless_RGB16_red_0.01.webp", 407 Color4f(0.01f, 0f, 0f, 1f) 408 ); 409 }); 410 f.scenario("Green 1.0", { 411 colorTestLossless!(PixelFormat.RGB16)( 412 "lossless_RGB16_green.webp", 413 Color4f(0f, 1f, 0f, 1f) 414 ); 415 }); 416 f.scenario("Blue 1.0", { 417 colorTestLossless!(PixelFormat.RGB16)( 418 "lossless_RGB16_blue.webp", 419 Color4f(0f, 0f, 1f, 1f) 420 ); 421 }); 422 f.scenario("Random.", { 423 const fn = "lossless_RGB16_random.webp"; 424 SuperImage source = RandomImages.circles!(PixelFormat.RGB16)(500, 400); 425 source.saveLosslessWEBP(fn); 426 SuperImage result = loadWEBP(fn); 427 // WebP supports only 8 bits per channel anyway. 428 // And alpha channel will equal 1, just like in the source image. 429 assertTheSame8bitWithAlpha(source, result); 430 }); 431 }); 432 } 433 } 434 435 436 /* 437 unittest { 438 import randomdlibimage; 439 440 SuperImage input = RandomImages.circles(500, 400); 441 string filename = "test_simple.webp"; 442 saveIt(input, filename); 443 444 auto inputL8 = convert!(Image!(PixelFormat.L8))(RandomImages.circles(500, 400)); 445 saveIt(inputL8, "test_L8.webp"); 446 447 auto inputLA8 = convert!(Image!(PixelFormat.LA8))(RandomImages.circles(500, 400)); 448 saveIt(inputLA8, "test_LA8.webp"); 449 450 auto inputRgba16 = convert!(Image!(PixelFormat.RGBA16))(RandomImages.circles(1920, 1080)); 451 saveIt(inputRgba16, "test_RGBA16.webp"); 452 } 453 454 */