1 /*
2     This file is part of darktable,
3     Copyright (C) 2013-2021 darktable developers.
4 
5     darktable is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     darktable is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #ifdef HAVE_CONFIG_H
20 #include "config.h"
21 #endif
22 #include "bauhaus/bauhaus.h"
23 #include "common/darktable.h"
24 #include "common/exif.h"
25 #include "common/imageio.h"
26 #include "common/imageio_module.h"
27 #include "control/conf.h"
28 #include "imageio/format/imageio_format_api.h"
29 #include <stdio.h>
30 #include <stdlib.h>
31 
32 #include <webp/encode.h>
33 
34 DT_MODULE(2)
35 
36 typedef enum
37 {
38   webp_lossy = 0,
39   webp_lossless = 1
40 } comp_type_t;
41 
42 
43 typedef enum
44 {
45   hint_default,
46   hint_picture,
47   hint_photo,
48   hint_graphic
49 } hint_t;
50 
51 
52 typedef struct dt_imageio_webp_t
53 {
54   dt_imageio_module_data_t global;
55   int comp_type;
56   int quality;
57   int hint;
58 } dt_imageio_webp_t;
59 
60 typedef struct dt_imageio_webp_gui_data_t
61 {
62   GtkWidget *compression;
63   GtkWidget *quality;
64   GtkWidget *hint;
65 } dt_imageio_webp_gui_data_t;
66 
67 #define _stringify(a) #a
68 #define stringify(a) _stringify(a)
69 
70 static const char *const EncoderError[] = {
71   "ok",
72   "out_of_memory: out of memory allocating objects",
73   "bitstream_out_of_memory: out of memory re-allocating byte buffer",
74   "null_parameter: null parameter passed to function",
75   "invalid_configuration: configuration is invalid",
76   "bad_dimension: bad picture dimension. maximum width and height "
77   "allowed is " stringify(WEBP_MAX_DIMENSION) " pixels.",
78   "partition0_overflow: partition #0 is too big to fit 512k.\n"
79   "to reduce the size of this partition, try using less segments "
80   "with the -segments option, and eventually reduce the number of "
81   "header bits using -partition_limit. more details are available "
82   "in the manual (`man cwebp`)",
83   "partition_overflow: partition is too big to fit 16M",
84   "bad_write: picture writer returned an i/o error",
85   "file_too_big: file would be too big to fit in 4G",
86   "user_abort: encoding abort requested by user"
87 };
88 
get_error_str(int err)89 const char *get_error_str(int err)
90 {
91   if (err < 0 || err >= sizeof(EncoderError)/sizeof(EncoderError[0]))
92   {
93     return "unknown error (err=%d). consider filling a bug to DT to update the webp error list";
94   }
95   return EncoderError[err];
96 }
97 
init(dt_imageio_module_format_t * self)98 void init(dt_imageio_module_format_t *self)
99 {
100 #ifdef USE_LUA
101   luaA_enum(darktable.lua_state.state, comp_type_t);
102   luaA_enum_value(darktable.lua_state.state, comp_type_t, webp_lossy);
103   luaA_enum_value(darktable.lua_state.state, comp_type_t, webp_lossless);
104   dt_lua_register_module_member(darktable.lua_state.state, self, dt_imageio_webp_t, comp_type, comp_type_t);
105   dt_lua_register_module_member(darktable.lua_state.state, self, dt_imageio_webp_t, quality, int);
106   luaA_enum(darktable.lua_state.state, hint_t);
107   luaA_enum_value(darktable.lua_state.state, hint_t, hint_default);
108   luaA_enum_value(darktable.lua_state.state, hint_t, hint_picture);
109   luaA_enum_value(darktable.lua_state.state, hint_t, hint_photo);
110   luaA_enum_value(darktable.lua_state.state, hint_t, hint_graphic);
111   dt_lua_register_module_member(darktable.lua_state.state, self, dt_imageio_webp_t, hint, hint_t);
112 #endif
113 }
cleanup(dt_imageio_module_format_t * self)114 void cleanup(dt_imageio_module_format_t *self)
115 {
116 }
117 
FileWriter(const uint8_t * data,size_t data_size,const WebPPicture * const pic)118 static int FileWriter(const uint8_t *data, size_t data_size, const WebPPicture *const pic)
119 {
120   FILE *const out = (FILE *)pic->custom_ptr;
121   return data_size ? (fwrite(data, data_size, 1, out) == 1) : 1;
122 }
123 
write_image(dt_imageio_module_data_t * webp,const char * filename,const void * in_tmp,dt_colorspaces_color_profile_type_t over_type,const char * over_filename,void * exif,int exif_len,int imgid,int num,int total,struct dt_dev_pixelpipe_t * pipe,const gboolean export_masks)124 int write_image(dt_imageio_module_data_t *webp, const char *filename, const void *in_tmp,
125                 dt_colorspaces_color_profile_type_t over_type, const char *over_filename,
126                 void *exif, int exif_len, int imgid, int num, int total, struct dt_dev_pixelpipe_t *pipe,
127                 const gboolean export_masks)
128 {
129   FILE *out = NULL;
130   WebPPicture pic;
131   int pic_init = 0;
132 
133   dt_imageio_webp_t *webp_data = (dt_imageio_webp_t *)webp;
134   out = g_fopen(filename, "w+b");
135   if (!out)
136   {
137     fprintf(stderr, "[webp export] error saving to %s\n", filename);
138     goto error;
139   }
140 
141   // Create, configure and validate a WebPConfig instance
142   WebPConfig config;
143   if(!WebPConfigPreset(&config, webp_data->hint, (float)webp_data->quality)) goto error;
144 
145   // TODO(jinxos): expose more config options in the UI
146   config.lossless = webp_data->comp_type;
147   config.image_hint = webp_data->hint;
148   config.method = 6;
149 
150   // these are to allow for large image export.
151   // TODO(jinxos): these values should be adjusted as needed and ideally determined at runtime.
152   config.segments = 4;
153   config.partition_limit = 70;
154   if(!WebPValidateConfig(&config))
155   {
156     fprintf(stderr, "[webp export] error validating encoder configuration\n");
157     goto error;
158   }
159 
160   if(!WebPPictureInit(&pic)) goto error;
161   pic_init = 1;
162   pic.width = webp_data->global.width;
163   pic.height = webp_data->global.height;
164   pic.use_argb = !!(config.lossless);
165   pic.writer = FileWriter;
166   pic.custom_ptr = out;
167 
168   WebPPictureImportRGBX(&pic, (const uint8_t *)in_tmp, webp_data->global.width * 4);
169   if(!config.lossless)
170   {
171     // webp is more efficient at coding YUV images, as we go lossy
172     // let the encoder where best to spend its bits instead of forcing it
173     // to spend bits equally on RGB data that doesn't weight the same when
174     // considering the human visual system.
175     WebPPictureARGBToYUVA(&pic, WEBP_YUV420A);
176   }
177 
178   if(!WebPEncode(&config, &pic))
179   {
180     fprintf(stderr, "[webp export] error during encoding (err:%d - %s)\n",
181             pic.error_code, get_error_str(pic.error_code));
182     goto error;
183   }
184 
185   WebPPictureFree(&pic);
186   fclose(out);
187 
188   dt_exif_write_blob(exif, exif_len, filename, 1);
189 
190   return 0;
191 
192 error:
193   if (pic_init) WebPPictureFree(&pic);
194   if(out) fclose(out);
195   return 1;
196 }
197 
params_size(dt_imageio_module_format_t * self)198 size_t params_size(dt_imageio_module_format_t *self)
199 {
200   return sizeof(dt_imageio_webp_t);
201 }
202 
legacy_params(dt_imageio_module_format_t * self,const void * const old_params,const size_t old_params_size,const int old_version,const int new_version,size_t * new_size)203 void *legacy_params(dt_imageio_module_format_t *self, const void *const old_params,
204                     const size_t old_params_size, const int old_version, const int new_version,
205                     size_t *new_size)
206 {
207   if(old_version == 1 && new_version == 2)
208   {
209     typedef struct dt_imageio_webp_v1_t
210     {
211       int max_width, max_height;
212       int width, height;
213       char style[128];
214       int comp_type;
215       int quality;
216       int hint;
217     } dt_imageio_webp_v1_t;
218 
219     dt_imageio_webp_v1_t *o = (dt_imageio_webp_v1_t *)old_params;
220     dt_imageio_webp_t *n = (dt_imageio_webp_t *)malloc(sizeof(dt_imageio_webp_t));
221 
222     n->global.max_width = o->max_width;
223     n->global.max_height = o->max_height;
224     n->global.width = o->width;
225     n->global.height = o->height;
226     g_strlcpy(n->global.style, o->style, sizeof(o->style));
227     n->global.style_append = FALSE;
228     n->comp_type = o->comp_type;
229     n->quality = o->quality;
230     n->hint = o->hint;
231     *new_size = self->params_size(self);
232     return n;
233   }
234   return NULL;
235 }
236 
get_params(dt_imageio_module_format_t * self)237 void *get_params(dt_imageio_module_format_t *self)
238 {
239   dt_imageio_webp_t *d = (dt_imageio_webp_t *)calloc(1, sizeof(dt_imageio_webp_t));
240   d->comp_type = dt_conf_get_int("plugins/imageio/format/webp/comp_type");
241   if(d->comp_type == webp_lossy)
242     d->quality = dt_conf_get_int("plugins/imageio/format/webp/quality");
243   else
244     d->quality = 100;
245   d->hint = dt_conf_get_int("plugins/imageio/format/webp/hint");
246   return d;
247 }
248 
set_params(dt_imageio_module_format_t * self,const void * params,const int size)249 int set_params(dt_imageio_module_format_t *self, const void *params, const int size)
250 {
251   if(size != self->params_size(self)) return 1;
252   const dt_imageio_webp_t *d = (dt_imageio_webp_t *)params;
253   dt_imageio_webp_gui_data_t *g = (dt_imageio_webp_gui_data_t *)self->gui_data;
254   dt_bauhaus_combobox_set(g->compression, d->comp_type);
255   dt_bauhaus_slider_set(g->quality, d->quality);
256   dt_bauhaus_combobox_set(g->hint, d->hint);
257   return 0;
258 }
259 
free_params(dt_imageio_module_format_t * self,dt_imageio_module_data_t * params)260 void free_params(dt_imageio_module_format_t *self, dt_imageio_module_data_t *params)
261 {
262   free(params);
263 }
264 
bpp(dt_imageio_module_data_t * p)265 int bpp(dt_imageio_module_data_t *p)
266 {
267   return 8;
268 }
269 
levels(dt_imageio_module_data_t * p)270 int levels(dt_imageio_module_data_t *p)
271 {
272   return IMAGEIO_RGB | IMAGEIO_INT8;
273 }
274 
mime(dt_imageio_module_data_t * data)275 const char *mime(dt_imageio_module_data_t *data)
276 {
277   // TODO: revisit this when IANA makes it official.
278   return "image/webp";
279 }
280 
extension(dt_imageio_module_data_t * data)281 const char *extension(dt_imageio_module_data_t *data)
282 {
283   return "webp";
284 }
285 
name()286 const char *name()
287 {
288   return _("WebP (8-bit)");
289 }
290 
compression_changed(GtkWidget * widget,gpointer user_data)291 static void compression_changed(GtkWidget *widget, gpointer user_data)
292 {
293   const int comp_type = dt_bauhaus_combobox_get(widget);
294   dt_conf_set_int("plugins/imageio/format/webp/comp_type", comp_type);
295 
296   if (comp_type == webp_lossless)
297     gtk_widget_set_sensitive(GTK_WIDGET(user_data), FALSE);
298   else
299     gtk_widget_set_sensitive(GTK_WIDGET(user_data), TRUE);
300 }
301 
quality_changed(GtkWidget * slider,gpointer user_data)302 static void quality_changed(GtkWidget *slider, gpointer user_data)
303 {
304   const int quality = (int)dt_bauhaus_slider_get(slider);
305   dt_conf_set_int("plugins/imageio/format/webp/quality", quality);
306 }
307 
hint_combobox_changed(GtkWidget * widget,gpointer user_data)308 static void hint_combobox_changed(GtkWidget *widget, gpointer user_data)
309 {
310   const int hint = dt_bauhaus_combobox_get(widget);
311   dt_conf_set_int("plugins/imageio/format/webp/hint", hint);
312 }
313 
gui_init(dt_imageio_module_format_t * self)314 void gui_init(dt_imageio_module_format_t *self)
315 {
316   dt_imageio_webp_gui_data_t *gui = (dt_imageio_webp_gui_data_t *)malloc(sizeof(dt_imageio_webp_gui_data_t));
317   self->gui_data = (void *)gui;
318   const int comp_type = dt_conf_get_int("plugins/imageio/format/webp/comp_type");
319   const int quality = dt_conf_get_int("plugins/imageio/format/webp/quality");
320   const int hint = dt_conf_get_int("plugins/imageio/format/webp/hint");
321 
322   self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
323 
324   gui->compression = dt_bauhaus_combobox_new(NULL);
325   dt_bauhaus_widget_set_label(gui->compression, NULL, N_("compression type"));
326   dt_bauhaus_combobox_add(gui->compression, _("lossy"));
327   dt_bauhaus_combobox_add(gui->compression, _("lossless"));
328   dt_bauhaus_combobox_set(gui->compression, comp_type);
329   gtk_box_pack_start(GTK_BOX(self->widget), gui->compression, TRUE, TRUE, 0);
330 
331   gui->quality = dt_bauhaus_slider_new_with_range(NULL,
332                                                   dt_confgen_get_int("plugins/imageio/format/webp/quality", DT_MIN),
333                                                   dt_confgen_get_int("plugins/imageio/format/webp/quality", DT_MAX),
334                                                   1,
335                                                   dt_confgen_get_int("plugins/imageio/format/webp/quality", DT_DEFAULT),
336                                                   0);
337   dt_bauhaus_widget_set_label(gui->quality, NULL, N_("quality"));
338   dt_bauhaus_slider_set_default(gui->quality, dt_confgen_get_int("plugins/imageio/format/webp/quality", DT_DEFAULT));
339   dt_bauhaus_slider_set_format(gui->quality, "%.2f%%");
340   gtk_widget_set_tooltip_text(gui->quality, _("applies only to lossy setting"));
341   if(quality > 0 && quality <= 100) dt_bauhaus_slider_set(gui->quality, quality);
342   gtk_box_pack_start(GTK_BOX(self->widget), gui->quality, TRUE, TRUE, 0);
343   g_signal_connect(G_OBJECT(gui->quality), "value-changed", G_CALLBACK(quality_changed), (gpointer)0);
344 
345   g_signal_connect(G_OBJECT(gui->compression), "value-changed", G_CALLBACK(compression_changed), (gpointer)gui->quality);
346 
347   if (comp_type == webp_lossless)
348     gtk_widget_set_sensitive(gui->quality, FALSE);
349 
350   gui->hint = dt_bauhaus_combobox_new(NULL);
351   dt_bauhaus_widget_set_label(gui->hint, NULL, N_("image hint"));
352   gtk_widget_set_tooltip_text(gui->hint,
353                _("image characteristics hint for the underlying encoder.\n"
354                "picture : digital picture, like portrait, inner shot\n"
355                "photo   : outdoor photograph, with natural lighting\n"
356                "graphic : discrete tone image (graph, map-tile etc)"));
357   dt_bauhaus_combobox_add(gui->hint, _("default"));
358   dt_bauhaus_combobox_add(gui->hint, _("picture"));
359   dt_bauhaus_combobox_add(gui->hint, _("photo"));
360   dt_bauhaus_combobox_add(gui->hint, _("graphic"));
361   dt_bauhaus_combobox_set(gui->hint, hint);
362   gtk_box_pack_start(GTK_BOX(self->widget), gui->hint, TRUE, TRUE, 0);
363   g_signal_connect(G_OBJECT(gui->hint), "value-changed", G_CALLBACK(hint_combobox_changed), NULL);
364 }
365 
gui_cleanup(dt_imageio_module_format_t * self)366 void gui_cleanup(dt_imageio_module_format_t *self)
367 {
368   free(self->gui_data);
369 }
370 
gui_reset(dt_imageio_module_format_t * self)371 void gui_reset(dt_imageio_module_format_t *self)
372 {
373   dt_imageio_webp_gui_data_t *gui = (dt_imageio_webp_gui_data_t *)self->gui_data;
374   const int comp_type = dt_confgen_get_int("plugins/imageio/format/webp/comp_type", DT_DEFAULT);
375   const int quality = dt_confgen_get_int("plugins/imageio/format/webp/quality", DT_DEFAULT);
376   const int hint = dt_confgen_get_int("plugins/imageio/format/webp/hint", DT_DEFAULT);
377   dt_bauhaus_combobox_set(gui->compression, comp_type);
378   dt_bauhaus_slider_set(gui->quality, quality);
379   dt_bauhaus_combobox_set(gui->hint, hint);
380 }
381 
flags(dt_imageio_module_data_t * data)382 int flags(dt_imageio_module_data_t *data)
383 {
384   // TODO(jinxos): support embedded XMP/ICC
385   return 0;
386 }
387 
388 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
389 // vim: shiftwidth=2 expandtab tabstop=2 cindent
390 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
391