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 */