libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

file-jxl-save.cc (30036B)


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