1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file.
5 
6 #include "plugins/gimp/file-jxl-save.h"
7 
8 #include <cmath>
9 
10 #include "gobject/gsignal.h"
11 
12 #define PLUG_IN_BINARY "file-jxl"
13 #define SAVE_PROC "file-jxl-save"
14 
15 #define SCALE_WIDTH 200
16 
17 namespace jxl {
18 
19 namespace {
20 
21 #ifndef g_clear_signal_handler
22 // g_clear_signal_handler was added in glib 2.62
g_clear_signal_handler(gulong * handler,gpointer instance)23 void g_clear_signal_handler(gulong* handler, gpointer instance) {
24   if (handler != nullptr && *handler != 0) {
25     g_signal_handler_disconnect(instance, *handler);
26     *handler = 0;
27   }
28 }
29 #endif  // g_clear_signal_handler
30 
31 class JpegXlSaveOpts {
32  public:
33   float distance;
34   float quality;
35 
36   bool lossless = false;
37   bool is_linear = false;
38   bool has_alpha = false;
39   bool is_gray = false;
40   bool icc_attached = false;
41 
42   bool advanced_mode = false;
43   bool use_container = true;
44   bool save_exif = false;
45   int encoding_effort = 7;
46   int faster_decoding = 0;
47 
48   std::string babl_format_str = "RGB u16";
49   std::string babl_type_str = "u16";
50   std::string babl_model_str = "RGB";
51 
52   JxlPixelFormat pixel_format;
53   JxlBasicInfo basic_info;
54 
55   // functions
56   JpegXlSaveOpts();
57 
58   bool SetDistance(float dist);
59   bool SetQuality(float qual);
60   bool SetDimensions(int x, int y);
61   bool SetNumChannels(int channels);
62 
63   bool UpdateDistance();
64   bool UpdateQuality();
65 
66   bool SetModel(bool is_linear_);
67 
68   bool UpdateBablFormat();
69   bool SetBablModel(std::string model);
70   bool SetBablType(std::string type);
71 
72   bool SetPrecision(int gimp_precision);
73 
74  private:
75 };  // class JpegXlSaveOpts
76 
77 JpegXlSaveOpts jxl_save_opts;
78 
79 class JpegXlSaveGui {
80  public:
81   bool SaveDialog();
82 
83  private:
84   GtkWidget* toggle_lossless = nullptr;
85   GtkAdjustment* entry_distance = nullptr;
86   GtkAdjustment* entry_quality = nullptr;
87   GtkAdjustment* entry_effort = nullptr;
88   GtkAdjustment* entry_faster = nullptr;
89   GtkWidget* frame_advanced = nullptr;
90   GtkWidget* toggle_no_xyb = nullptr;
91   GtkWidget* toggle_raw = nullptr;
92   gulong handle_toggle_lossless = 0;
93   gulong handle_entry_quality = 0;
94   gulong handle_entry_distance = 0;
95 
96   static bool GuiOnChangeQuality(GtkAdjustment* adj_qual, void* this_pointer);
97 
98   static bool GuiOnChangeDistance(GtkAdjustment* adj_dist, void* this_pointer);
99 
100   static bool GuiOnChangeEffort(GtkAdjustment* adj_effort);
101   static bool GuiOnChangeLossless(GtkWidget* toggle, void* this_pointer);
102   static bool GuiOnChangeCodestream(GtkWidget* toggle);
103   static bool GuiOnChangeNoXYB(GtkWidget* toggle);
104 
105   static bool GuiOnChangeAdvancedMode(GtkWidget* toggle, void* this_pointer);
106 };  // class JpegXlSaveGui
107 
108 JpegXlSaveGui jxl_save_gui;
109 
GuiOnChangeQuality(GtkAdjustment * adj_qual,void * this_pointer)110 bool JpegXlSaveGui::GuiOnChangeQuality(GtkAdjustment* adj_qual,
111                                        void* this_pointer) {
112   JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
113 
114   g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
115   g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
116   g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
117 
118   GtkAdjustment* adj_dist = self->entry_distance;
119   jxl_save_opts.quality = gtk_adjustment_get_value(adj_qual);
120   jxl_save_opts.UpdateDistance();
121   gtk_adjustment_set_value(adj_dist, jxl_save_opts.distance);
122 
123   self->handle_toggle_lossless = g_signal_connect(
124       self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
125   self->handle_entry_distance =
126       g_signal_connect(self->entry_distance, "value-changed",
127                        G_CALLBACK(GuiOnChangeDistance), self);
128   self->handle_entry_quality =
129       g_signal_connect(self->entry_quality, "value-changed",
130                        G_CALLBACK(GuiOnChangeQuality), self);
131   return true;
132 }
133 
GuiOnChangeDistance(GtkAdjustment * adj_dist,void * this_pointer)134 bool JpegXlSaveGui::GuiOnChangeDistance(GtkAdjustment* adj_dist,
135                                         void* this_pointer) {
136   JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
137   GtkAdjustment* adj_qual = self->entry_quality;
138 
139   g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
140   g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
141   g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
142 
143   jxl_save_opts.distance = gtk_adjustment_get_value(adj_dist);
144   jxl_save_opts.UpdateQuality();
145   gtk_adjustment_set_value(adj_qual, jxl_save_opts.quality);
146 
147   if (!(jxl_save_opts.distance < 0.001)) {
148     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_lossless),
149                                  false);
150     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
151   }
152 
153   self->handle_toggle_lossless = g_signal_connect(
154       self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
155   self->handle_entry_distance =
156       g_signal_connect(self->entry_distance, "value-changed",
157                        G_CALLBACK(GuiOnChangeDistance), self);
158   self->handle_entry_quality =
159       g_signal_connect(self->entry_quality, "value-changed",
160                        G_CALLBACK(GuiOnChangeQuality), self);
161   return true;
162 }
163 
GuiOnChangeEffort(GtkAdjustment * adj_effort)164 bool JpegXlSaveGui::GuiOnChangeEffort(GtkAdjustment* adj_effort) {
165   float new_effort = 10 - gtk_adjustment_get_value(adj_effort);
166   jxl_save_opts.encoding_effort = new_effort;
167   return true;
168 }
169 
GuiOnChangeLossless(GtkWidget * toggle,void * this_pointer)170 bool JpegXlSaveGui::GuiOnChangeLossless(GtkWidget* toggle, void* this_pointer) {
171   JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
172   GtkAdjustment* adj_distance = self->entry_distance;
173   GtkAdjustment* adj_quality = self->entry_quality;
174   GtkAdjustment* adj_effort = self->entry_effort;
175 
176   jxl_save_opts.lossless =
177       gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
178 
179   g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
180   g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
181   g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
182 
183   if (jxl_save_opts.lossless) {
184     gtk_adjustment_set_value(adj_quality, 100.0);
185     gtk_adjustment_set_value(adj_distance, 0.0);
186     jxl_save_opts.distance = 0;
187     jxl_save_opts.UpdateQuality();
188     gtk_adjustment_set_value(adj_effort, 7);
189     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), true);
190   } else {
191     gtk_adjustment_set_value(adj_quality, 90.0);
192     gtk_adjustment_set_value(adj_distance, 1.0);
193     jxl_save_opts.distance = 1.0;
194     jxl_save_opts.UpdateQuality();
195     gtk_adjustment_set_value(adj_effort, 3);
196     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
197   }
198   self->handle_toggle_lossless = g_signal_connect(
199       self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
200   self->handle_entry_distance =
201       g_signal_connect(self->entry_distance, "value-changed",
202                        G_CALLBACK(GuiOnChangeDistance), self);
203   self->handle_entry_quality =
204       g_signal_connect(self->entry_quality, "value-changed",
205                        G_CALLBACK(GuiOnChangeQuality), self);
206   return true;
207 }
208 
GuiOnChangeCodestream(GtkWidget * toggle)209 bool JpegXlSaveGui::GuiOnChangeCodestream(GtkWidget* toggle) {
210   jxl_save_opts.use_container =
211       !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
212   return true;
213 }
214 
GuiOnChangeNoXYB(GtkWidget * toggle)215 bool JpegXlSaveGui::GuiOnChangeNoXYB(GtkWidget* toggle) {
216   jxl_save_opts.basic_info.uses_original_profile =
217       gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
218   return true;
219 }
220 
GuiOnChangeAdvancedMode(GtkWidget * toggle,void * this_pointer)221 bool JpegXlSaveGui::GuiOnChangeAdvancedMode(GtkWidget* toggle,
222                                             void* this_pointer) {
223   JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
224   jxl_save_opts.advanced_mode =
225       gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
226 
227   gtk_widget_set_sensitive(self->frame_advanced, jxl_save_opts.advanced_mode);
228 
229   if (!jxl_save_opts.advanced_mode) {
230     jxl_save_opts.basic_info.uses_original_profile = false;
231     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
232 
233     jxl_save_opts.use_container = true;
234     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_raw), false);
235 
236     jxl_save_opts.faster_decoding = 0;
237     gtk_adjustment_set_value(GTK_ADJUSTMENT(self->entry_faster), 0);
238   }
239   return true;
240 }
241 
SaveDialog()242 bool JpegXlSaveGui::SaveDialog() {
243   gboolean run;
244   GtkWidget* dialog;
245   GtkWidget* content_area;
246   GtkWidget* main_vbox;
247   GtkWidget* frame;
248   GtkWidget* toggle;
249   GtkWidget* table;
250   GtkWidget* vbox;
251   GtkWidget* separator;
252 
253   // initialize export dialog
254   gimp_ui_init(PLUG_IN_BINARY, true);
255   dialog = gimp_export_dialog_new("JPEG XL", PLUG_IN_BINARY, SAVE_PROC);
256 
257   gtk_window_set_resizable(GTK_WINDOW(dialog), false);
258   content_area = gimp_export_dialog_get_content_area(dialog);
259 
260   main_vbox = gtk_vbox_new(false, 6);
261   gtk_container_set_border_width(GTK_CONTAINER(main_vbox), 6);
262   gtk_box_pack_start(GTK_BOX(content_area), main_vbox, true, true, 0);
263   gtk_widget_show(main_vbox);
264 
265   // Standard Settings Frame
266   frame = gtk_frame_new(nullptr);
267   gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_ETCHED_IN);
268   gtk_box_pack_start(GTK_BOX(main_vbox), frame, false, false, 0);
269   gtk_widget_show(frame);
270 
271   vbox = gtk_vbox_new(false, 6);
272   gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
273   gtk_container_add(GTK_CONTAINER(frame), vbox);
274   gtk_widget_show(vbox);
275 
276   // Layout Table
277   table = gtk_table_new(20, 3, false);
278   gtk_table_set_col_spacings(GTK_TABLE(table), 6);
279   gtk_box_pack_start(GTK_BOX(vbox), table, false, false, 0);
280   gtk_widget_show(table);
281 
282   // Distance Slider
283   static gchar distance_help[] =
284       "Butteraugli distance target.  Suggested values:"
285       "\n\td\u00A0=\u00A00.3\tExcellent"
286       "\n\td\u00A0=\u00A01\tVery Good"
287       "\n\td\u00A0=\u00A02\tGood"
288       "\n\td\u00A0=\u00A03\tFair"
289       "\n\td\u00A0=\u00A06\tPoor";
290 
291   entry_distance = (GtkAdjustment*)gimp_scale_entry_new(
292       GTK_TABLE(table), 0, 0, "Distance", SCALE_WIDTH, 0,
293       jxl_save_opts.distance, 0.0, 15.0, 0.001, 1.0, 3, true, 0.0, 0.0,
294       distance_help, SAVE_PROC);
295   gimp_scale_entry_set_logarithmic((GtkObject*)entry_distance, true);
296 
297   // Quality Slider
298   static gchar quality_help[] =
299       "JPEG-style Quality is remapped to distance.  "
300       "Values roughly match libjpeg quality settings.";
301   entry_quality = (GtkAdjustment*)gimp_scale_entry_new(
302       GTK_TABLE(table), 0, 1, "Quality", SCALE_WIDTH, 0, jxl_save_opts.quality,
303       8.26, 100.0, 1.0, 10.0, 2, true, 0.0, 0.0, quality_help, SAVE_PROC);
304 
305   // Distance and Quality Signals
306   handle_entry_distance = g_signal_connect(
307       entry_distance, "value-changed", G_CALLBACK(GuiOnChangeDistance), this);
308   handle_entry_quality = g_signal_connect(entry_quality, "value-changed",
309                                           G_CALLBACK(GuiOnChangeQuality), this);
310 
311   // ----------
312   separator = gtk_vseparator_new();
313   gtk_table_attach(GTK_TABLE(table), separator, 0, 2, 2, 3, GTK_EXPAND,
314                    GTK_EXPAND, 9, 9);
315   gtk_widget_show(separator);
316 
317   // Encoding Effort / Speed
318   static gchar effort_help[] =
319       "Adjust encoding speed.  Higher values are faster because "
320       "the encoder uses less effort to hit distance targets.  "
321       "As\u00A0a\u00A0result, image quality may be decreased.  "
322       "Default\u00A0=\u00A03.";
323   entry_effort = (GtkAdjustment*)gimp_scale_entry_new(
324       GTK_TABLE(table), 0, 3, "Speed", SCALE_WIDTH, 0,
325       10 - jxl_save_opts.encoding_effort, 1, 9, 1, 2, 0, true, 0.0, 0.0,
326       effort_help, SAVE_PROC);
327 
328   // effort signal
329   g_signal_connect(entry_effort, "value-changed", G_CALLBACK(GuiOnChangeEffort),
330                    nullptr);
331 
332   // ----------
333   separator = gtk_vseparator_new();
334   gtk_table_attach(GTK_TABLE(table), separator, 0, 2, 4, 5, GTK_EXPAND,
335                    GTK_EXPAND, 9, 9);
336   gtk_widget_show(separator);
337 
338   // Lossless Mode Convenience Checkbox
339   static gchar lossless_help[] =
340       "Compress using modular lossless mode.  "
341       "Speed\u00A0is adjusted to improve performance.";
342   toggle_lossless = gtk_check_button_new_with_label("Lossless Mode");
343   gimp_help_set_help_data(toggle_lossless, lossless_help, nullptr);
344   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_lossless),
345                                jxl_save_opts.lossless);
346   gtk_table_attach_defaults(GTK_TABLE(table), toggle_lossless, 0, 2, 5, 6);
347   gtk_widget_show(toggle_lossless);
348 
349   // lossless signal
350   handle_toggle_lossless = g_signal_connect(
351       toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), this);
352 
353   // ----------
354   separator = gtk_vseparator_new();
355   gtk_box_pack_start(GTK_BOX(main_vbox), separator, false, false, 1);
356   gtk_widget_show(separator);
357 
358   // Advanced Settings Frame
359   std::vector<GtkWidget*> advanced_opts;
360 
361   frame_advanced = gtk_frame_new("Advanced Settings");
362   gimp_help_set_help_data(frame_advanced,
363                           "Some advanced settings may produce malformed files.",
364                           nullptr);
365   gtk_frame_set_shadow_type(GTK_FRAME(frame_advanced), GTK_SHADOW_ETCHED_IN);
366   gtk_box_pack_start(GTK_BOX(main_vbox), frame_advanced, true, true, 0);
367   gtk_widget_show(frame_advanced);
368 
369   gtk_widget_set_sensitive(frame_advanced, false);
370 
371   vbox = gtk_vbox_new(false, 6);
372   gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
373   gtk_container_add(GTK_CONTAINER(frame_advanced), vbox);
374   gtk_widget_show(vbox);
375 
376   // uses_original_profile
377   static gchar uses_original_profile_help[] =
378       "Prevents conversion to the XYB colorspace.  "
379       "File sizes are approximately doubled.";
380   toggle_no_xyb = gtk_check_button_new_with_label("Do not use XYB colorspace");
381   gimp_help_set_help_data(toggle_no_xyb, uses_original_profile_help, nullptr);
382   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_no_xyb),
383                                jxl_save_opts.basic_info.uses_original_profile);
384   gtk_box_pack_start(GTK_BOX(vbox), toggle_no_xyb, false, false, 0);
385   gtk_widget_show(toggle_no_xyb);
386 
387   g_signal_connect(toggle_no_xyb, "toggled", G_CALLBACK(GuiOnChangeNoXYB),
388                    nullptr);
389 
390   // save raw codestream
391   static gchar codestream_help[] =
392       "Save the raw codestream, without a container.  "
393       "The container is required for metadata and some other features.";
394   toggle_raw = gtk_check_button_new_with_label("Save Raw Codestream");
395   gimp_help_set_help_data(toggle_raw, codestream_help, nullptr);
396   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_raw),
397                                !jxl_save_opts.use_container);
398   gtk_box_pack_start(GTK_BOX(vbox), toggle_raw, false, false, 0);
399   gtk_widget_show(toggle_raw);
400 
401   g_signal_connect(toggle_raw, "toggled", G_CALLBACK(GuiOnChangeCodestream),
402                    nullptr);
403 
404   // ----------
405   separator = gtk_vseparator_new();
406   gtk_box_pack_start(GTK_BOX(vbox), separator, false, false, 1);
407   gtk_widget_show(separator);
408 
409   // Faster Decoding / Decoding Speed
410   static gchar faster_help[] =
411       "Improve decoding speed at the expense of quality.  "
412       "Default\u00A0=\u00A00.";
413   table = gtk_table_new(1, 3, false);
414   gtk_table_set_col_spacings(GTK_TABLE(table), 6);
415   gtk_container_add(GTK_CONTAINER(vbox), table);
416   gtk_widget_show(table);
417 
418   entry_faster = (GtkAdjustment*)gimp_scale_entry_new(
419       GTK_TABLE(table), 0, 0, "Faster Decoding", SCALE_WIDTH, 0,
420       jxl_save_opts.faster_decoding, 0, 4, 1, 1, 0, true, 0.0, 0.0, faster_help,
421       SAVE_PROC);
422 
423   // Faster Decoding Signals
424   g_signal_connect(entry_faster, "value-changed",
425                    G_CALLBACK(gimp_int_adjustment_update),
426                    &jxl_save_opts.faster_decoding);
427 
428   // Enable Advanced Settings
429   frame = gtk_frame_new(nullptr);
430   gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE);
431   gtk_box_pack_start(GTK_BOX(main_vbox), frame, true, true, 0);
432   gtk_widget_show(frame);
433 
434   vbox = gtk_vbox_new(false, 6);
435   gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
436   gtk_container_add(GTK_CONTAINER(frame), vbox);
437   gtk_widget_show(vbox);
438 
439   static gchar advanced_help[] =
440       "Some advanced settings may produce malformed files.";
441   toggle = gtk_check_button_new_with_label("Enable Advanced Settings");
442   gimp_help_set_help_data(toggle, advanced_help, nullptr);
443   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle),
444                                jxl_save_opts.advanced_mode);
445   gtk_box_pack_start(GTK_BOX(vbox), toggle, false, false, 0);
446   gtk_widget_show(toggle);
447 
448   g_signal_connect(toggle, "toggled", G_CALLBACK(GuiOnChangeAdvancedMode),
449                    this);
450 
451   // show dialog
452   gtk_widget_show(dialog);
453 
454   GtkAllocation allocation;
455   gtk_widget_get_allocation(dialog, &allocation);
456 
457   int height = allocation.height;
458   gtk_widget_set_size_request(dialog, height * 1.5, height);
459 
460   run = (gimp_dialog_run(GIMP_DIALOG(dialog)) == GTK_RESPONSE_OK);
461   gtk_widget_destroy(dialog);
462 
463   return run;
464 }  // JpegXlSaveGui::SaveDialog
465 
JpegXlSaveOpts()466 JpegXlSaveOpts::JpegXlSaveOpts() {
467   SetDistance(1.0);
468 
469   pixel_format.num_channels = 4;
470   pixel_format.data_type = JXL_TYPE_FLOAT;
471   pixel_format.endianness = JXL_NATIVE_ENDIAN;
472   pixel_format.align = 0;
473 
474   JxlEncoderInitBasicInfo(&basic_info);
475   return;
476 }  // JpegXlSaveOpts constructor
477 
SetModel(bool is_linear_)478 bool JpegXlSaveOpts::SetModel(bool is_linear_) {
479   int channels;
480   std::string model;
481 
482   if (is_gray) {
483     channels = 1;
484     if (is_linear_) {
485       model = "Y";
486     } else {
487       model = "Y'";
488     }
489   } else {
490     channels = 3;
491     if (is_linear_) {
492       model = "RGB";
493     } else {
494       model = "R'G'B'";
495     }
496   }
497   if (has_alpha) {
498     SetBablModel(model + "A");
499     SetNumChannels(channels + 1);
500   } else {
501     SetBablModel(model);
502     SetNumChannels(channels);
503   }
504   return true;
505 }  // JpegXlSaveOpts::SetModel
506 
SetDistance(float dist)507 bool JpegXlSaveOpts::SetDistance(float dist) {
508   distance = dist;
509   return UpdateQuality();
510 }
511 
SetQuality(float qual)512 bool JpegXlSaveOpts::SetQuality(float qual) {
513   quality = qual;
514   return UpdateDistance();
515 }
516 
UpdateQuality()517 bool JpegXlSaveOpts::UpdateQuality() {
518   float qual;
519 
520   if (distance < 0.1) {
521     qual = 100;
522   } else if (distance > 6.56) {
523     qual = 30 - 5 * log(abs(6.25 * distance - 40)) / log(2.5);
524     lossless = false;
525   } else {
526     qual = 100 - (distance - 0.1) / 0.09;
527     lossless = false;
528   }
529 
530   if (qual < 0) {
531     quality = 0.0;
532   } else if (qual >= 100) {
533     quality = 100.0;
534   } else {
535     quality = qual;
536   }
537 
538   return true;
539 }
540 
UpdateDistance()541 bool JpegXlSaveOpts::UpdateDistance() {
542   float dist;
543   if (quality >= 30) {
544     dist = 0.1 + (100 - quality) * 0.09;
545   } else {
546     dist = 6.4 + pow(2.5, (30 - quality) / 5.0) / 6.25;
547   }
548 
549   if (dist > 15) {
550     distance = 15;
551   } else {
552     distance = dist;
553   }
554   return true;
555 }
556 
SetDimensions(int x,int y)557 bool JpegXlSaveOpts::SetDimensions(int x, int y) {
558   basic_info.xsize = x;
559   basic_info.ysize = y;
560   return true;
561 }
562 
SetNumChannels(int channels)563 bool JpegXlSaveOpts::SetNumChannels(int channels) {
564   switch (channels) {
565     case 1:
566       pixel_format.num_channels = 1;
567       basic_info.num_color_channels = 1;
568       basic_info.num_extra_channels = 0;
569       basic_info.alpha_bits = 0;
570       basic_info.alpha_exponent_bits = 0;
571       break;
572     case 2:
573       pixel_format.num_channels = 2;
574       basic_info.num_color_channels = 1;
575       basic_info.num_extra_channels = 1;
576       basic_info.alpha_bits = int(std::fmin(16, basic_info.bits_per_sample));
577       basic_info.alpha_exponent_bits = 0;
578       break;
579     case 3:
580       pixel_format.num_channels = 3;
581       basic_info.num_color_channels = 3;
582       basic_info.num_extra_channels = 0;
583       basic_info.alpha_bits = 0;
584       basic_info.alpha_exponent_bits = 0;
585       break;
586     case 4:
587       pixel_format.num_channels = 4;
588       basic_info.num_color_channels = 3;
589       basic_info.num_extra_channels = 1;
590       basic_info.alpha_bits = int(std::fmin(16, basic_info.bits_per_sample));
591       basic_info.alpha_exponent_bits = 0;
592       break;
593     default:
594       SetNumChannels(3);
595   }  // switch
596   return true;
597 }  // JpegXlSaveOpts::SetNumChannels
598 
UpdateBablFormat()599 bool JpegXlSaveOpts::UpdateBablFormat() {
600   babl_format_str = babl_model_str + " " + babl_type_str;
601   return true;
602 }
603 
SetBablModel(std::string model)604 bool JpegXlSaveOpts::SetBablModel(std::string model) {
605   babl_model_str = model;
606   return UpdateBablFormat();
607 }
608 
SetBablType(std::string type)609 bool JpegXlSaveOpts::SetBablType(std::string type) {
610   babl_type_str = type;
611   return UpdateBablFormat();
612 }
613 
SetPrecision(int gimp_precision)614 bool JpegXlSaveOpts::SetPrecision(int gimp_precision) {
615   switch (gimp_precision) {
616     case GIMP_PRECISION_HALF_GAMMA:
617     case GIMP_PRECISION_HALF_LINEAR:
618       basic_info.bits_per_sample = 16;
619       basic_info.exponent_bits_per_sample = 5;
620       break;
621 
622     // UINT32 not supported by encoder; using FLOAT instead
623     case GIMP_PRECISION_U32_GAMMA:
624     case GIMP_PRECISION_U32_LINEAR:
625     case GIMP_PRECISION_FLOAT_GAMMA:
626     case GIMP_PRECISION_FLOAT_LINEAR:
627       basic_info.bits_per_sample = 32;
628       basic_info.exponent_bits_per_sample = 8;
629       break;
630 
631     case GIMP_PRECISION_U16_GAMMA:
632     case GIMP_PRECISION_U16_LINEAR:
633       basic_info.bits_per_sample = 16;
634       basic_info.exponent_bits_per_sample = 0;
635       break;
636 
637     default:
638     case GIMP_PRECISION_U8_LINEAR:
639     case GIMP_PRECISION_U8_GAMMA:
640       basic_info.bits_per_sample = 8;
641       basic_info.exponent_bits_per_sample = 0;
642       break;
643   }
644   return true;
645 }  // JpegXlSaveOpts::SetPrecision
646 
647 }  // namespace
648 
SaveJpegXlImage(const gint32 image_id,const gint32 drawable_id,const gint32 orig_image_id,const gchar * const filename)649 bool SaveJpegXlImage(const gint32 image_id, const gint32 drawable_id,
650                      const gint32 orig_image_id, const gchar* const filename) {
651   if (!jxl_save_gui.SaveDialog()) {
652     return true;
653   }
654 
655   gint32 nlayers;
656   gint32* layers;
657   gint32 duplicate = gimp_image_duplicate(image_id);
658 
659   JpegXlGimpProgress gimp_save_progress(
660       ("Saving JPEG XL file:" + std::string(filename)).c_str());
661   gimp_save_progress.update();
662 
663   // try to get ICC color profile...
664   std::vector<uint8_t> icc;
665 
666   GimpColorProfile* profile = gimp_image_get_effective_color_profile(image_id);
667   jxl_save_opts.is_gray = gimp_color_profile_is_gray(profile);
668   jxl_save_opts.is_linear = gimp_color_profile_is_linear(profile);
669 
670   profile = gimp_image_get_color_profile(image_id);
671   if (profile) {
672     g_printerr(SAVE_PROC " Info: Extracting ICC Profile...\n");
673     gsize icc_size;
674     const guint8* const icc_bytes =
675         gimp_color_profile_get_icc_profile(profile, &icc_size);
676 
677     icc.assign(icc_bytes, icc_bytes + icc_size);
678   } else {
679     g_printerr(SAVE_PROC " Info: No ICC profile.  Exporting image anyway.\n");
680   }
681 
682   gimp_save_progress.update();
683 
684   jxl_save_opts.SetDimensions(gimp_image_width(image_id),
685                               gimp_image_height(image_id));
686 
687   jxl_save_opts.SetPrecision(gimp_image_get_precision(image_id));
688   layers = gimp_image_get_layers(duplicate, &nlayers);
689 
690   for (int i = 0; i < nlayers; i++) {
691     if (gimp_drawable_has_alpha(layers[i])) {
692       jxl_save_opts.has_alpha = true;
693       break;
694     }
695   }
696 
697   gimp_save_progress.update();
698 
699   // layers need to match image size, for now
700   for (int i = 0; i < nlayers; i++) {
701     gimp_layer_resize_to_image_size(layers[i]);
702   }
703 
704   // treat layers as animation frames, for now
705   if (nlayers > 1) {
706     jxl_save_opts.basic_info.have_animation = true;
707     jxl_save_opts.basic_info.animation.tps_numerator = 100;
708   }
709 
710   gimp_save_progress.update();
711 
712   // multi-threaded parallel runner.
713   auto runner = JxlResizableParallelRunnerMake(nullptr);
714 
715   JxlResizableParallelRunnerSetThreads(
716       runner.get(),
717       JxlResizableParallelRunnerSuggestThreads(jxl_save_opts.basic_info.xsize,
718                                                jxl_save_opts.basic_info.ysize));
719 
720   auto enc = JxlEncoderMake(/*memory_manager=*/nullptr);
721   JxlEncoderUseContainer(enc.get(), jxl_save_opts.use_container);
722 
723   if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(),
724                                                      JxlResizableParallelRunner,
725                                                      runner.get())) {
726     g_printerr(SAVE_PROC " Error: JxlEncoderSetParallelRunner failed\n");
727     return false;
728   }
729 
730   // try to use ICC profile
731   if (icc.size() > 0 && !jxl_save_opts.is_gray) {
732     if (JXL_ENC_SUCCESS ==
733         JxlEncoderSetICCProfile(enc.get(), icc.data(), icc.size())) {
734       jxl_save_opts.icc_attached = true;
735     } else {
736       g_printerr(SAVE_PROC " Warning: JxlEncoderSetICCProfile failed.\n");
737       jxl_save_opts.basic_info.uses_original_profile = false;
738       jxl_save_opts.lossless = false;
739     }
740   } else {
741     g_printerr(SAVE_PROC " Warning: Using internal profile.\n");
742     jxl_save_opts.basic_info.uses_original_profile = false;
743     jxl_save_opts.lossless = false;
744   }
745 
746   // set up internal color profile
747   JxlColorEncoding color_encoding = {};
748 
749   if (jxl_save_opts.is_linear) {
750     JxlColorEncodingSetToLinearSRGB(&color_encoding, jxl_save_opts.is_gray);
751   } else {
752     JxlColorEncodingSetToSRGB(&color_encoding, jxl_save_opts.is_gray);
753   }
754 
755   if (JXL_ENC_SUCCESS !=
756       JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) {
757     g_printerr(SAVE_PROC " Warning: JxlEncoderSetColorEncoding failed\n");
758   }
759 
760   // set encoder options
761   JxlEncoderFrameSettings* frame_settings;
762   frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
763 
764   JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT,
765                                    jxl_save_opts.encoding_effort);
766   JxlEncoderFrameSettingsSetOption(frame_settings,
767                                    JXL_ENC_FRAME_SETTING_DECODING_SPEED,
768                                    jxl_save_opts.faster_decoding);
769 
770   // lossless mode
771   if (jxl_save_opts.lossless || jxl_save_opts.distance < 0.01) {
772     if (jxl_save_opts.basic_info.exponent_bits_per_sample > 0) {
773       // lossless mode doesn't work well with floating point
774       jxl_save_opts.distance = 0.01;
775       jxl_save_opts.lossless = false;
776       JxlEncoderSetFrameLossless(frame_settings, false);
777       JxlEncoderSetFrameDistance(frame_settings, 0.01);
778     } else {
779       JxlEncoderSetFrameDistance(frame_settings, 0);
780       JxlEncoderSetFrameLossless(frame_settings, true);
781     }
782   } else {
783     jxl_save_opts.lossless = false;
784     JxlEncoderSetFrameLossless(frame_settings, false);
785     JxlEncoderSetFrameDistance(frame_settings, jxl_save_opts.distance);
786   }
787 
788   // this sets some basic_info properties
789   jxl_save_opts.SetModel(jxl_save_opts.is_linear);
790 
791   if (JXL_ENC_SUCCESS !=
792       JxlEncoderSetBasicInfo(enc.get(), &jxl_save_opts.basic_info)) {
793     g_printerr(SAVE_PROC " Error: JxlEncoderSetBasicInfo failed\n");
794     return false;
795   }
796 
797   // convert precision and colorspace
798   if (jxl_save_opts.is_linear &&
799       jxl_save_opts.basic_info.bits_per_sample < 32) {
800     gimp_image_convert_precision(duplicate, GIMP_PRECISION_FLOAT_LINEAR);
801   } else {
802     gimp_image_convert_precision(duplicate, GIMP_PRECISION_FLOAT_GAMMA);
803   }
804 
805   // process layers and compress into JXL
806   size_t buffer_size =
807       jxl_save_opts.basic_info.xsize * jxl_save_opts.basic_info.ysize *
808       jxl_save_opts.pixel_format.num_channels * 4;  // bytes per sample
809 
810   for (int i = nlayers - 1; i >= 0; i--) {
811     gimp_save_progress.update();
812 
813     // copy image into buffer...
814     gpointer pixels_buffer_1;
815     gpointer pixels_buffer_2;
816     pixels_buffer_1 = g_malloc(buffer_size);
817     pixels_buffer_2 = g_malloc(buffer_size);
818 
819     gimp_layer_resize_to_image_size(layers[i]);
820 
821     GeglBuffer* buffer = gimp_drawable_get_buffer(layers[i]);
822 
823     // using gegl_buffer_set_format to get the format because
824     // gegl_buffer_get_format doesn't always get the original format
825     const Babl* native_format = gegl_buffer_set_format(buffer, nullptr);
826 
827     gegl_buffer_get(buffer,
828                     GEGL_RECTANGLE(0, 0, jxl_save_opts.basic_info.xsize,
829                                    jxl_save_opts.basic_info.ysize),
830                     1.0, native_format, pixels_buffer_1, GEGL_AUTO_ROWSTRIDE,
831                     GEGL_ABYSS_NONE);
832     g_clear_object(&buffer);
833 
834     // use babl to fix gamma mismatch issues
835     if (jxl_save_opts.icc_attached) {
836       jxl_save_opts.SetModel(jxl_save_opts.is_linear);
837     } else {
838       jxl_save_opts.SetModel(!jxl_save_opts.is_linear);
839     }
840     jxl_save_opts.pixel_format.data_type = JXL_TYPE_FLOAT;
841     jxl_save_opts.SetBablType("float");
842     const Babl* destination_format =
843         babl_format(jxl_save_opts.babl_format_str.c_str());
844 
845     babl_process(
846         babl_fish(native_format, destination_format), pixels_buffer_1,
847         pixels_buffer_2,
848         jxl_save_opts.basic_info.xsize * jxl_save_opts.basic_info.ysize);
849 
850     gimp_save_progress.update();
851 
852     // send layer to encoder
853     if (JXL_ENC_SUCCESS !=
854         JxlEncoderAddImageFrame(frame_settings, &jxl_save_opts.pixel_format,
855                                 pixels_buffer_2, buffer_size)) {
856       g_printerr(SAVE_PROC " Error: JxlEncoderAddImageFrame failed\n");
857       return false;
858     }
859   }
860 
861   JxlEncoderCloseInput(enc.get());
862 
863   // get data from encoder
864   std::vector<uint8_t> compressed;
865   compressed.resize(262144);
866   uint8_t* next_out = compressed.data();
867   size_t avail_out = compressed.size();
868 
869   JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT;
870   while (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
871     gimp_save_progress.update();
872 
873     process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out);
874     if (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
875       size_t offset = next_out - compressed.data();
876       compressed.resize(compressed.size() + 262144);
877       next_out = compressed.data() + offset;
878       avail_out = compressed.size() - offset;
879     }
880   }
881   compressed.resize(next_out - compressed.data());
882 
883   if (JXL_ENC_SUCCESS != process_result) {
884     g_printerr(SAVE_PROC " Error: JxlEncoderProcessOutput failed\n");
885     return false;
886   }
887 
888   // write file
889   std::ofstream outstream(filename, std::ios::out | std::ios::binary);
890   copy(compressed.begin(), compressed.end(),
891        std::ostream_iterator<uint8_t>(outstream));
892 
893   gimp_save_progress.finished();
894   return true;
895 }  // SaveJpegXlImage()
896 
897 }  // namespace jxl
898