// 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 #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(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(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(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(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 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 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.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); } // 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(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 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(outstream)); gimp_save_progress.finished(); return true; } // SaveJpegXlImage() } // namespace jxl