diff options
Diffstat (limited to 'third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc')
-rw-r--r-- | third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc | 895 |
1 files changed, 895 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc b/third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc new file mode 100644 index 0000000000..c1e1ebd9af --- /dev/null +++ b/third_party/jpeg-xl/plugins/gimp/file-jxl-save.cc @@ -0,0 +1,895 @@ +// 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 <jxl/encode.h> +#include <jxl/encode_cxx.h> + +#include <cmath> +#include <utility> + +#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.SetQuality(gtk_adjustment_get_value(adj_qual)); + 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.SetDistance(gtk_adjustment_get_value(adj_dist)); + 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.4) { + qual = -5.0 / 53.0 * sqrt(6360.0 * distance - 39975.0) + 1725.0 / 53.0; + 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 = 53.0 / 3000.0 * quality * quality - 23.0 / 20.0 * quality + 25.0; + } + + if (dist > 25) { + distance = 25; + } 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 = std::move(model); + return UpdateBablFormat(); +} + +bool JpegXlSaveOpts::SetBablType(std::string type) { + babl_type_str = std::move(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; + } + + // 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; + } + + // try to use ICC profile + if (!icc.empty() && !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 + JxlEncoderFrameSettings* frame_settings; + frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr); + + JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, + jxl_save_opts.encoding_effort); + JxlEncoderFrameSettingsSetOption(frame_settings, + JXL_ENC_FRAME_SETTING_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; + JxlEncoderSetFrameLossless(frame_settings, false); + JxlEncoderSetFrameDistance(frame_settings, 0.01); + } else { + JxlEncoderSetFrameDistance(frame_settings, 0); + JxlEncoderSetFrameLossless(frame_settings, true); + } + } else { + jxl_save_opts.lossless = false; + JxlEncoderSetFrameLossless(frame_settings, false); + JxlEncoderSetFrameDistance(frame_settings, jxl_save_opts.distance); + } + + // 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 + 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(frame_settings, &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 |