text: // Copyright (c) the JPEG XL Project Authors. All rights reserved.
  //
  // Use of this source code is governed by a BSD-style
  // license that can be found in the LICENSE file.
  
  #include "plugins/gimp/file-jxl-save.h"
  
  #include <cmath>
  
  #include "gobject/gsignal.h"
  
  #define PLUG_IN_BINARY "file-jxl"
  #define SAVE_PROC "file-jxl-save"
  
  #define SCALE_WIDTH 200
  
  namespace jxl {
  
  namespace {
  
  #ifndef g_clear_signal_handler
  // g_clear_signal_handler was added in glib 2.62
  void g_clear_signal_handler(gulong* handler, gpointer instance) {
    if (handler != nullptr && *handler != 0) {
      g_signal_handler_disconnect(instance, *handler);
      *handler = 0;
    }
  }
  #endif  // g_clear_signal_handler
  
  class JpegXlSaveOpts {
   public:
    float distance;
    float quality;
  
    bool lossless = false;
    bool is_linear = false;
    bool has_alpha = false;
    bool is_gray = false;
    bool icc_attached = false;
  
    bool advanced_mode = false;
    bool use_container = true;
    bool save_exif = false;
    int encoding_effort = 7;
    int faster_decoding = 0;
  
    std::string babl_format_str = "RGB u16";
    std::string babl_type_str = "u16";
    std::string babl_model_str = "RGB";
  
    JxlPixelFormat pixel_format;
    JxlBasicInfo basic_info;
  
    // functions
    JpegXlSaveOpts();
  
    bool SetDistance(float dist);
    bool SetQuality(float qual);
    bool SetDimensions(int x, int y);
    bool SetNumChannels(int channels);
  
    bool UpdateDistance();
    bool UpdateQuality();
  
    bool SetModel(bool is_linear_);
  
    bool UpdateBablFormat();
    bool SetBablModel(std::string model);
    bool SetBablType(std::string type);
  
    bool SetPrecision(int gimp_precision);
  
   private:
  };  // class JpegXlSaveOpts
  
  JpegXlSaveOpts jxl_save_opts;
  
  class JpegXlSaveGui {
   public:
    bool SaveDialog();
  
   private:
    GtkWidget* toggle_lossless = nullptr;
    GtkAdjustment* entry_distance = nullptr;
    GtkAdjustment* entry_quality = nullptr;
    GtkAdjustment* entry_effort = nullptr;
    GtkAdjustment* entry_faster = nullptr;
    GtkWidget* frame_advanced = nullptr;
    GtkWidget* toggle_no_xyb = nullptr;
    GtkWidget* toggle_raw = nullptr;
    gulong handle_toggle_lossless = 0;
    gulong handle_entry_quality = 0;
    gulong handle_entry_distance = 0;
  
    static bool GuiOnChangeQuality(GtkAdjustment* adj_qual, void* this_pointer);
  
    static bool GuiOnChangeDistance(GtkAdjustment* adj_dist, void* this_pointer);
  
    static bool GuiOnChangeEffort(GtkAdjustment* adj_effort);
    static bool GuiOnChangeLossless(GtkWidget* toggle, void* this_pointer);
    static bool GuiOnChangeCodestream(GtkWidget* toggle);
    static bool GuiOnChangeNoXYB(GtkWidget* toggle);
  
    static bool GuiOnChangeAdvancedMode(GtkWidget* toggle, void* this_pointer);
  };  // class JpegXlSaveGui
  
  JpegXlSaveGui jxl_save_gui;
  
  bool JpegXlSaveGui::GuiOnChangeQuality(GtkAdjustment* adj_qual,
                                         void* this_pointer) {
    JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
  
    g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
    g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
    g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
  
    GtkAdjustment* adj_dist = self->entry_distance;
    jxl_save_opts.quality = gtk_adjustment_get_value(adj_qual);
    jxl_save_opts.UpdateDistance();
    gtk_adjustment_set_value(adj_dist, jxl_save_opts.distance);
  
    self->handle_toggle_lossless = g_signal_connect(
        self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
    self->handle_entry_distance =
        g_signal_connect(self->entry_distance, "value-changed",
                         G_CALLBACK(GuiOnChangeDistance), self);
    self->handle_entry_quality =
        g_signal_connect(self->entry_quality, "value-changed",
                         G_CALLBACK(GuiOnChangeQuality), self);
    return true;
  }
  
  bool JpegXlSaveGui::GuiOnChangeDistance(GtkAdjustment* adj_dist,
                                          void* this_pointer) {
    JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
    GtkAdjustment* adj_qual = self->entry_quality;
  
    g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
    g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
    g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
  
    jxl_save_opts.distance = gtk_adjustment_get_value(adj_dist);
    jxl_save_opts.UpdateQuality();
    gtk_adjustment_set_value(adj_qual, jxl_save_opts.quality);
  
    if (!(jxl_save_opts.distance < 0.001)) {
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_lossless),
                                   false);
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
    }
  
    self->handle_toggle_lossless = g_signal_connect(
        self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
    self->handle_entry_distance =
        g_signal_connect(self->entry_distance, "value-changed",
                         G_CALLBACK(GuiOnChangeDistance), self);
    self->handle_entry_quality =
        g_signal_connect(self->entry_quality, "value-changed",
                         G_CALLBACK(GuiOnChangeQuality), self);
    return true;
  }
  
  bool JpegXlSaveGui::GuiOnChangeEffort(GtkAdjustment* adj_effort) {
    float new_effort = 10 - gtk_adjustment_get_value(adj_effort);
    jxl_save_opts.encoding_effort = new_effort;
    return true;
  }
  
  bool JpegXlSaveGui::GuiOnChangeLossless(GtkWidget* toggle, void* this_pointer) {
    JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
    GtkAdjustment* adj_distance = self->entry_distance;
    GtkAdjustment* adj_quality = self->entry_quality;
    GtkAdjustment* adj_effort = self->entry_effort;
  
    jxl_save_opts.lossless =
        gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
  
    g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance);
    g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality);
    g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless);
  
    if (jxl_save_opts.lossless) {
      gtk_adjustment_set_value(adj_quality, 100.0);
      gtk_adjustment_set_value(adj_distance, 0.0);
      jxl_save_opts.distance = 0;
      jxl_save_opts.UpdateQuality();
      gtk_adjustment_set_value(adj_effort, 7);
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), true);
    } else {
      gtk_adjustment_set_value(adj_quality, 90.0);
      gtk_adjustment_set_value(adj_distance, 1.0);
      jxl_save_opts.distance = 1.0;
      jxl_save_opts.UpdateQuality();
      gtk_adjustment_set_value(adj_effort, 3);
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
    }
    self->handle_toggle_lossless = g_signal_connect(
        self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self);
    self->handle_entry_distance =
        g_signal_connect(self->entry_distance, "value-changed",
                         G_CALLBACK(GuiOnChangeDistance), self);
    self->handle_entry_quality =
        g_signal_connect(self->entry_quality, "value-changed",
                         G_CALLBACK(GuiOnChangeQuality), self);
    return true;
  }
  
  bool JpegXlSaveGui::GuiOnChangeCodestream(GtkWidget* toggle) {
    jxl_save_opts.use_container =
        !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
    return true;
  }
  
  bool JpegXlSaveGui::GuiOnChangeNoXYB(GtkWidget* toggle) {
    jxl_save_opts.basic_info.uses_original_profile =
        gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
    return true;
  }
  
  bool JpegXlSaveGui::GuiOnChangeAdvancedMode(GtkWidget* toggle,
                                              void* this_pointer) {
    JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer);
    jxl_save_opts.advanced_mode =
        gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle));
  
    gtk_widget_set_sensitive(self->frame_advanced, jxl_save_opts.advanced_mode);
  
    if (!jxl_save_opts.advanced_mode) {
      jxl_save_opts.basic_info.uses_original_profile = false;
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false);
  
      jxl_save_opts.use_container = true;
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_raw), false);
  
      jxl_save_opts.faster_decoding = 0;
      gtk_adjustment_set_value(GTK_ADJUSTMENT(self->entry_faster), 0);
    }
    return true;
  }
  
  bool JpegXlSaveGui::SaveDialog() {
    gboolean run;
    GtkWidget* dialog;
    GtkWidget* content_area;
    GtkWidget* main_vbox;
    GtkWidget* frame;
    GtkWidget* toggle;
    GtkWidget* table;
    GtkWidget* vbox;
    GtkWidget* separator;
  
    // initialize export dialog
    gimp_ui_init(PLUG_IN_BINARY, true);
    dialog = gimp_export_dialog_new("JPEG XL", PLUG_IN_BINARY, SAVE_PROC);
  
    gtk_window_set_resizable(GTK_WINDOW(dialog), false);
    content_area = gimp_export_dialog_get_content_area(dialog);
  
    main_vbox = gtk_vbox_new(false, 6);
    gtk_container_set_border_width(GTK_CONTAINER(main_vbox), 6);
    gtk_box_pack_start(GTK_BOX(content_area), main_vbox, true, true, 0);
    gtk_widget_show(main_vbox);
  
    // Standard Settings Frame
    frame = gtk_frame_new(nullptr);
    gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_ETCHED_IN);
    gtk_box_pack_start(GTK_BOX(main_vbox), frame, false, false, 0);
    gtk_widget_show(frame);
  
    vbox = gtk_vbox_new(false, 6);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
    gtk_container_add(GTK_CONTAINER(frame), vbox);
    gtk_widget_show(vbox);
  
    // Layout Table
    table = gtk_table_new(20, 3, false);
    gtk_table_set_col_spacings(GTK_TABLE(table), 6);
    gtk_box_pack_start(GTK_BOX(vbox), table, false, false, 0);
    gtk_widget_show(table);
  
    // Distance Slider
    static gchar distance_help[] =
        "Butteraugli distance target.  Suggested values:"
        "\n\td\u00A0=\u00A00.3\tExcellent"
        "\n\td\u00A0=\u00A01\tVery Good"
        "\n\td\u00A0=\u00A02\tGood"
        "\n\td\u00A0=\u00A03\tFair"
        "\n\td\u00A0=\u00A06\tPoor";
  
    entry_distance = (GtkAdjustment*)gimp_scale_entry_new(
        GTK_TABLE(table), 0, 0, "Distance", SCALE_WIDTH, 0,
        jxl_save_opts.distance, 0.0, 15.0, 0.001, 1.0, 3, true, 0.0, 0.0,
        distance_help, SAVE_PROC);
    gimp_scale_entry_set_logarithmic((GtkObject*)entry_distance, true);
  
    // Quality Slider
    static gchar quality_help[] =
        "JPEG-style Quality is remapped to distance.  "
        "Values roughly match libjpeg quality settings.";
    entry_quality = (GtkAdjustment*)gimp_scale_entry_new(
        GTK_TABLE(table), 0, 1, "Quality", SCALE_WIDTH, 0, jxl_save_opts.quality,
        8.26, 100.0, 1.0, 10.0, 2, true, 0.0, 0.0, quality_help, SAVE_PROC);
  
    // Distance and Quality Signals
    handle_entry_distance = g_signal_connect(
        entry_distance, "value-changed", G_CALLBACK(GuiOnChangeDistance), this);
    handle_entry_quality = g_signal_connect(entry_quality, "value-changed",
                                            G_CALLBACK(GuiOnChangeQuality), this);
  
    // ----------
    separator = gtk_vseparator_new();
    gtk_table_attach(GTK_TABLE(table), separator, 0, 2, 2, 3, GTK_EXPAND,
                     GTK_EXPAND, 9, 9);
    gtk_widget_show(separator);
  
    // Encoding Effort / Speed
    static gchar effort_help[] =
        "Adjust encoding speed.  Higher values are faster because "
        "the encoder uses less effort to hit distance targets.  "
        "As\u00A0a\u00A0result, image quality may be decreased.  "
        "Default\u00A0=\u00A03.";
    entry_effort = (GtkAdjustment*)gimp_scale_entry_new(
        GTK_TABLE(table), 0, 3, "Speed", SCALE_WIDTH, 0,
        10 - jxl_save_opts.encoding_effort, 1, 9, 1, 2, 0, true, 0.0, 0.0,
        effort_help, SAVE_PROC);
  
    // effort signal
    g_signal_connect(entry_effort, "value-changed", G_CALLBACK(GuiOnChangeEffort),
                     nullptr);
  
    // ----------
    separator = gtk_vseparator_new();
    gtk_table_attach(GTK_TABLE(table), separator, 0, 2, 4, 5, GTK_EXPAND,
                     GTK_EXPAND, 9, 9);
    gtk_widget_show(separator);
  
    // Lossless Mode Convenience Checkbox
    static gchar lossless_help[] =
        "Compress using modular lossless mode.  "
        "Speed\u00A0is adjusted to improve performance.";
    toggle_lossless = gtk_check_button_new_with_label("Lossless Mode");
    gimp_help_set_help_data(toggle_lossless, lossless_help, nullptr);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_lossless),
                                 jxl_save_opts.lossless);
    gtk_table_attach_defaults(GTK_TABLE(table), toggle_lossless, 0, 2, 5, 6);
    gtk_widget_show(toggle_lossless);
  
    // lossless signal
    handle_toggle_lossless = g_signal_connect(
        toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), this);
  
    // ----------
    separator = gtk_vseparator_new();
    gtk_box_pack_start(GTK_BOX(main_vbox), separator, false, false, 1);
    gtk_widget_show(separator);
  
    // Advanced Settings Frame
    std::vector<GtkWidget*> advanced_opts;
  
    frame_advanced = gtk_frame_new("Advanced Settings");
    gimp_help_set_help_data(frame_advanced,
                            "Some advanced settings may produce malformed files.",
                            nullptr);
    gtk_frame_set_shadow_type(GTK_FRAME(frame_advanced), GTK_SHADOW_ETCHED_IN);
    gtk_box_pack_start(GTK_BOX(main_vbox), frame_advanced, true, true, 0);
    gtk_widget_show(frame_advanced);
  
    gtk_widget_set_sensitive(frame_advanced, false);
  
    vbox = gtk_vbox_new(false, 6);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
    gtk_container_add(GTK_CONTAINER(frame_advanced), vbox);
    gtk_widget_show(vbox);
  
    // uses_original_profile
    static gchar uses_original_profile_help[] =
        "Prevents conversion to the XYB colorspace.  "
        "File sizes are approximately doubled.";
    toggle_no_xyb = gtk_check_button_new_with_label("Do not use XYB colorspace");
    gimp_help_set_help_data(toggle_no_xyb, uses_original_profile_help, nullptr);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_no_xyb),
                                 jxl_save_opts.basic_info.uses_original_profile);
    gtk_box_pack_start(GTK_BOX(vbox), toggle_no_xyb, false, false, 0);
    gtk_widget_show(toggle_no_xyb);
  
    g_signal_connect(toggle_no_xyb, "toggled", G_CALLBACK(GuiOnChangeNoXYB),
                     nullptr);
  
    // save raw codestream
    static gchar codestream_help[] =
        "Save the raw codestream, without a container.  "
        "The container is required for metadata and some other features.";
    toggle_raw = gtk_check_button_new_with_label("Save Raw Codestream");
    gimp_help_set_help_data(toggle_raw, codestream_help, nullptr);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_raw),
                                 !jxl_save_opts.use_container);
    gtk_box_pack_start(GTK_BOX(vbox), toggle_raw, false, false, 0);
    gtk_widget_show(toggle_raw);
  
    g_signal_connect(toggle_raw, "toggled", G_CALLBACK(GuiOnChangeCodestream),
                     nullptr);
  
    // ----------
    separator = gtk_vseparator_new();
    gtk_box_pack_start(GTK_BOX(vbox), separator, false, false, 1);
    gtk_widget_show(separator);
  
    // Faster Decoding / Decoding Speed
    static gchar faster_help[] =
        "Improve decoding speed at the expense of quality.  "
        "Default\u00A0=\u00A00.";
    table = gtk_table_new(1, 3, false);
    gtk_table_set_col_spacings(GTK_TABLE(table), 6);
    gtk_container_add(GTK_CONTAINER(vbox), table);
    gtk_widget_show(table);
  
    entry_faster = (GtkAdjustment*)gimp_scale_entry_new(
        GTK_TABLE(table), 0, 0, "Faster Decoding", SCALE_WIDTH, 0,
        jxl_save_opts.faster_decoding, 0, 4, 1, 1, 0, true, 0.0, 0.0, faster_help,
        SAVE_PROC);
  
    // Faster Decoding Signals
    g_signal_connect(entry_faster, "value-changed",
                     G_CALLBACK(gimp_int_adjustment_update),
                     &jxl_save_opts.faster_decoding);
  
    // Enable Advanced Settings
    frame = gtk_frame_new(nullptr);
    gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE);
    gtk_box_pack_start(GTK_BOX(main_vbox), frame, true, true, 0);
    gtk_widget_show(frame);
  
    vbox = gtk_vbox_new(false, 6);
    gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
    gtk_container_add(GTK_CONTAINER(frame), vbox);
    gtk_widget_show(vbox);
  
    static gchar advanced_help[] =
        "Some advanced settings may produce malformed files.";
    toggle = gtk_check_button_new_with_label("Enable Advanced Settings");
    gimp_help_set_help_data(toggle, advanced_help, nullptr);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle),
                                 jxl_save_opts.advanced_mode);
    gtk_box_pack_start(GTK_BOX(vbox), toggle, false, false, 0);
    gtk_widget_show(toggle);
  
    g_signal_connect(toggle, "toggled", G_CALLBACK(GuiOnChangeAdvancedMode),
                     this);
  
    // show dialog
    gtk_widget_show(dialog);
  
    GtkAllocation allocation;
    gtk_widget_get_allocation(dialog, &allocation);
  
    int height = allocation.height;
    gtk_widget_set_size_request(dialog, height * 1.5, height);
  
    run = (gimp_dialog_run(GIMP_DIALOG(dialog)) == GTK_RESPONSE_OK);
    gtk_widget_destroy(dialog);
  
    return run;
  }  // JpegXlSaveGui::SaveDialog
  
  JpegXlSaveOpts::JpegXlSaveOpts() {
    SetDistance(1.0);
  
    pixel_format.num_channels = 4;
    pixel_format.data_type = JXL_TYPE_FLOAT;
    pixel_format.endianness = JXL_NATIVE_ENDIAN;
    pixel_format.align = 0;
  
    JxlEncoderInitBasicInfo(&basic_info);
    return;
  }  // JpegXlSaveOpts constructor
  
  bool JpegXlSaveOpts::SetModel(bool is_linear_) {
    int channels;
    std::string model;
  
    if (is_gray) {
      channels = 1;
      if (is_linear_) {
        model = "Y";
      } else {
        model = "Y'";
      }
    } else {
      channels = 3;
      if (is_linear_) {
        model = "RGB";
      } else {
        model = "R'G'B'";
      }
    }
    if (has_alpha) {
      SetBablModel(model + "A");
      SetNumChannels(channels + 1);
    } else {
      SetBablModel(model);
      SetNumChannels(channels);
    }
    return true;
  }  // JpegXlSaveOpts::SetModel
  
  bool JpegXlSaveOpts::SetDistance(float dist) {
    distance = dist;
    return UpdateQuality();
  }
  
  bool JpegXlSaveOpts::SetQuality(float qual) {
    quality = qual;
    return UpdateDistance();
  }
  
  bool JpegXlSaveOpts::UpdateQuality() {
    float qual;
  
    if (distance < 0.1) {
      qual = 100;
    } else if (distance > 6.56) {
      qual = 30 - 5 * log(abs(6.25 * distance - 40)) / log(2.5);
      lossless = false;
    } else {
      qual = 100 - (distance - 0.1) / 0.09;
      lossless = false;
    }
  
    if (qual < 0) {
      quality = 0.0;
    } else if (qual >= 100) {
      quality = 100.0;
    } else {
      quality = qual;
    }
  
    return true;
  }
  
  bool JpegXlSaveOpts::UpdateDistance() {
    float dist;
    if (quality >= 30) {
      dist = 0.1 + (100 - quality) * 0.09;
    } else {
      dist = 6.4 + pow(2.5, (30 - quality) / 5.0) / 6.25;
    }
  
    if (dist > 15) {
      distance = 15;
    } else {
      distance = dist;
    }
    return true;
  }
  
  bool JpegXlSaveOpts::SetDimensions(int x, int y) {
    basic_info.xsize = x;
    basic_info.ysize = y;
    return true;
  }
  
  bool JpegXlSaveOpts::SetNumChannels(int channels) {
    switch (channels) {
      case 1:
        pixel_format.num_channels = 1;
        basic_info.num_color_channels = 1;
        basic_info.num_extra_channels = 0;
        basic_info.alpha_bits = 0;
        basic_info.alpha_exponent_bits = 0;
        break;
      case 2:
        pixel_format.num_channels = 2;
        basic_info.num_color_channels = 1;
        basic_info.num_extra_channels = 1;
        basic_info.alpha_bits = int(std::fmin(16, basic_info.bits_per_sample));
        basic_info.alpha_exponent_bits = 0;
        break;
      case 3:
        pixel_format.num_channels = 3;
        basic_info.num_color_channels = 3;
        basic_info.num_extra_channels = 0;
        basic_info.alpha_bits = 0;
        basic_info.alpha_exponent_bits = 0;
        break;
      case 4:
        pixel_format.num_channels = 4;
        basic_info.num_color_channels = 3;
        basic_info.num_extra_channels = 1;
        basic_info.alpha_bits = int(std::fmin(16, basic_info.bits_per_sample));
        basic_info.alpha_exponent_bits = 0;
        break;
      default:
        SetNumChannels(3);
    }  // switch
    return true;
  }  // JpegXlSaveOpts::SetNumChannels
  
  bool JpegXlSaveOpts::UpdateBablFormat() {
    babl_format_str = babl_model_str + " " + babl_type_str;
    return true;
  }
  
  bool JpegXlSaveOpts::SetBablModel(std::string model) {
    babl_model_str = model;
    return UpdateBablFormat();
  }
  
  bool JpegXlSaveOpts::SetBablType(std::string type) {
    babl_type_str = type;
    return UpdateBablFormat();
  }
  
  bool JpegXlSaveOpts::SetPrecision(int gimp_precision) {
    switch (gimp_precision) {
      case GIMP_PRECISION_HALF_GAMMA:
      case GIMP_PRECISION_HALF_LINEAR:
        basic_info.bits_per_sample = 16;
        basic_info.exponent_bits_per_sample = 5;
        break;
  
      // UINT32 not supported by encoder; using FLOAT instead
      case GIMP_PRECISION_U32_GAMMA:
      case GIMP_PRECISION_U32_LINEAR:
      case GIMP_PRECISION_FLOAT_GAMMA:
      case GIMP_PRECISION_FLOAT_LINEAR:
        basic_info.bits_per_sample = 32;
        basic_info.exponent_bits_per_sample = 8;
        break;
  
      case GIMP_PRECISION_U16_GAMMA:
      case GIMP_PRECISION_U16_LINEAR:
        basic_info.bits_per_sample = 16;
        basic_info.exponent_bits_per_sample = 0;
        break;
  
      default:
      case GIMP_PRECISION_U8_LINEAR:
      case GIMP_PRECISION_U8_GAMMA:
        basic_info.bits_per_sample = 8;
        basic_info.exponent_bits_per_sample = 0;
        break;
    }
    return true;
  }  // JpegXlSaveOpts::SetPrecision
  
  }  // namespace
  
  bool SaveJpegXlImage(const gint32 image_id, const gint32 drawable_id,
                       const gint32 orig_image_id, const gchar* const filename) {
    if (!jxl_save_gui.SaveDialog()) {
      return true;
    }
  
    gint32 nlayers;
    gint32* layers;
    gint32 duplicate = gimp_image_duplicate(image_id);
  
    JpegXlGimpProgress gimp_save_progress(
        ("Saving JPEG XL file:" + std::string(filename)).c_str());
    gimp_save_progress.update();
  
    // try to get ICC color profile...
    std::vector<uint8_t> icc;
  
    GimpColorProfile* profile = gimp_image_get_effective_color_profile(image_id);
    jxl_save_opts.is_gray = gimp_color_profile_is_gray(profile);
    jxl_save_opts.is_linear = gimp_color_profile_is_linear(profile);
  
    profile = gimp_image_get_color_profile(image_id);
    if (profile) {
      g_printerr(SAVE_PROC " Info: Extracting ICC Profile...\n");
      gsize icc_size;
      const guint8* const icc_bytes =
          gimp_color_profile_get_icc_profile(profile, &icc_size);
  
      icc.assign(icc_bytes, icc_bytes + icc_size);
    } else {
      g_printerr(SAVE_PROC " Info: No ICC profile.  Exporting image anyway.\n");
    }
  
    gimp_save_progress.update();
  
    jxl_save_opts.SetDimensions(gimp_image_width(image_id),
                                gimp_image_height(image_id));
  
    jxl_save_opts.SetPrecision(gimp_image_get_precision(image_id));
    layers = gimp_image_get_layers(duplicate, &nlayers);
  
    for (int i = 0; i < nlayers; i++) {
      if (gimp_drawable_has_alpha(layers[i])) {
        jxl_save_opts.has_alpha = true;
        break;
      }
    }
  
    gimp_save_progress.update();
  
    // layers need to match image size, for now
    for (int i = 0; i < nlayers; i++) {
      gimp_layer_resize_to_image_size(layers[i]);
    }
  
    // treat layers as animation frames, for now
    if (nlayers > 1) {
      jxl_save_opts.basic_info.have_animation = true;
      jxl_save_opts.basic_info.animation.tps_numerator = 100;
    }
  
    gimp_save_progress.update();
  
    // multi-threaded parallel runner.
    auto runner = JxlResizableParallelRunnerMake(nullptr);
  
    JxlResizableParallelRunnerSetThreads(
        runner.get(),
        JxlResizableParallelRunnerSuggestThreads(jxl_save_opts.basic_info.xsize,
                                                 jxl_save_opts.basic_info.ysize));
  
    auto enc = JxlEncoderMake(/*memory_manager=*/nullptr);
    JxlEncoderUseContainer(enc.get(), jxl_save_opts.use_container);
  
    if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(),
                                                       JxlResizableParallelRunner,
                                                       runner.get())) {
      g_printerr(SAVE_PROC " Error: JxlEncoderSetParallelRunner failed\n");
      return false;
    }
  
    // try to use ICC profile
    if (icc.size() > 0 && !jxl_save_opts.is_gray) {
      if (JXL_ENC_SUCCESS ==
          JxlEncoderSetICCProfile(enc.get(), icc.data(), icc.size())) {
        jxl_save_opts.icc_attached = true;
      } else {
        g_printerr(SAVE_PROC " Warning: JxlEncoderSetICCProfile failed.\n");
        jxl_save_opts.basic_info.uses_original_profile = false;
        jxl_save_opts.lossless = false;
      }
    } else {
      g_printerr(SAVE_PROC " Warning: Using internal profile.\n");
      jxl_save_opts.basic_info.uses_original_profile = false;
      jxl_save_opts.lossless = false;
    }
  
    // set up internal color profile
    JxlColorEncoding color_encoding = {};
  
    if (jxl_save_opts.is_linear) {
      JxlColorEncodingSetToLinearSRGB(&color_encoding, jxl_save_opts.is_gray);
    } else {
      JxlColorEncodingSetToSRGB(&color_encoding, jxl_save_opts.is_gray);
    }
  
    if (JXL_ENC_SUCCESS !=
        JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) {
      g_printerr(SAVE_PROC " Warning: JxlEncoderSetColorEncoding failed\n");
    }
  
    // set encoder options
    JxlEncoderOptions* enc_opts;
    enc_opts = JxlEncoderOptionsCreate(enc.get(), nullptr);
  
    JxlEncoderOptionsSetInteger(enc_opts, JXL_ENC_OPTION_EFFORT,
                                jxl_save_opts.encoding_effort);
    JxlEncoderOptionsSetInteger(enc_opts, JXL_ENC_OPTION_DECODING_SPEED,
                                jxl_save_opts.faster_decoding);
  
    // lossless mode
    if (jxl_save_opts.lossless || jxl_save_opts.distance < 0.01) {
      if (jxl_save_opts.basic_info.exponent_bits_per_sample > 0) {
        // lossless mode doesn't work well with floating point
        jxl_save_opts.distance = 0.01;
        jxl_save_opts.lossless = false;
        JxlEncoderOptionsSetLossless(enc_opts, false);
        JxlEncoderOptionsSetDistance(enc_opts, 0.01);
      } else {
        JxlEncoderOptionsSetDistance(enc_opts, 0);
        JxlEncoderOptionsSetLossless(enc_opts, true);
      }
    } else {
      jxl_save_opts.lossless = false;
      JxlEncoderOptionsSetLossless(enc_opts, false);
      JxlEncoderOptionsSetDistance(enc_opts, jxl_save_opts.distance);
    }
  
    // this sets some basic_info properties
    jxl_save_opts.SetModel(jxl_save_opts.is_linear);
  
    if (JXL_ENC_SUCCESS !=
        JxlEncoderSetBasicInfo(enc.get(), &jxl_save_opts.basic_info)) {
      g_printerr(SAVE_PROC " Error: JxlEncoderSetBasicInfo failed\n");
      return false;
    }
  
    // convert precision and colorspace
    if (jxl_save_opts.is_linear &&
        jxl_save_opts.basic_info.bits_per_sample < 32) {
      gimp_image_convert_precision(duplicate, GIMP_PRECISION_FLOAT_LINEAR);
    } else {
      gimp_image_convert_precision(duplicate, GIMP_PRECISION_FLOAT_GAMMA);
    }
  
    // process layers and compress into JXL
    size_t buffer_size =
        jxl_save_opts.basic_info.xsize * jxl_save_opts.basic_info.ysize *
        jxl_save_opts.pixel_format.num_channels * 4;  // bytes per sample
  
    for (int i = nlayers - 1; i >= 0; i--) {
      gimp_save_progress.update();
  
      // copy image into buffer...
      gpointer pixels_buffer_1;
      gpointer pixels_buffer_2;
      pixels_buffer_1 = g_malloc(buffer_size);
      pixels_buffer_2 = g_malloc(buffer_size);
  
      gimp_layer_resize_to_image_size(layers[i]);
  
      GeglBuffer* buffer = gimp_drawable_get_buffer(layers[i]);
  
      // using gegl_buffer_set_format to get the format because
      // gegl_buffer_get_format doesn't always get the original format
      const Babl* native_format = gegl_buffer_set_format(buffer, nullptr);
  
      gegl_buffer_get(buffer,
                      GEGL_RECTANGLE(0, 0, jxl_save_opts.basic_info.xsize,
                                     jxl_save_opts.basic_info.ysize),
                      1.0, native_format, pixels_buffer_1, GEGL_AUTO_ROWSTRIDE,
                      GEGL_ABYSS_NONE);
      g_clear_object(&buffer);
  
      // use babl to fix gamma mismatch issues
      if (jxl_save_opts.icc_attached) {
        jxl_save_opts.SetModel(jxl_save_opts.is_linear);
      } else {
        jxl_save_opts.SetModel(!jxl_save_opts.is_linear);
      }
      jxl_save_opts.pixel_format.data_type = JXL_TYPE_FLOAT;
      jxl_save_opts.SetBablType("float");
      const Babl* destination_format =
          babl_format(jxl_save_opts.babl_format_str.c_str());
  
      babl_process(
          babl_fish(native_format, destination_format), pixels_buffer_1,
          pixels_buffer_2,
          jxl_save_opts.basic_info.xsize * jxl_save_opts.basic_info.ysize);
  
      gimp_save_progress.update();
  
      // send layer to encoder
      if (JXL_ENC_SUCCESS !=
          JxlEncoderAddImageFrame(enc_opts, &jxl_save_opts.pixel_format,
                                  pixels_buffer_2, buffer_size)) {
        g_printerr(SAVE_PROC " Error: JxlEncoderAddImageFrame failed\n");
        return false;
      }
    }
  
    JxlEncoderCloseInput(enc.get());
  
    // get data from encoder
    std::vector<uint8_t> compressed;
    compressed.resize(262144);
    uint8_t* next_out = compressed.data();
    size_t avail_out = compressed.size();
  
    JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT;
    while (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
      gimp_save_progress.update();
  
      process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out);
      if (process_result == JXL_ENC_NEED_MORE_OUTPUT) {
        size_t offset = next_out - compressed.data();
        compressed.resize(compressed.size() + 262144);
        next_out = compressed.data() + offset;
        avail_out = compressed.size() - offset;
      }
    }
    compressed.resize(next_out - compressed.data());
  
    if (JXL_ENC_SUCCESS != process_result) {
      g_printerr(SAVE_PROC " Error: JxlEncoderProcessOutput failed\n");
      return false;
    }
  
    // write file
    std::ofstream outstream(filename, std::ios::out | std::ios::binary);
    copy(compressed.begin(), compressed.end(),
         std::ostream_iterator<uint8_t>(outstream));
  
    gimp_save_progress.finished();
    return true;
  }  // SaveJpegXlImage()
  
  }  // namespace jxl
  