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