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