893 lines
29 KiB
C++
893 lines
29 KiB
C++
// 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 <jxl/types.h>
|
|
|
|
#include <cmath>
|
|
#include <utility>
|
|
|
|
#include "gobject/gsignal.h"
|
|
|
|
#define PLUG_IN_BINARY "file-jxl"
|
|
#define SAVE_PROC "file-jxl-save"
|
|
|
|
namespace jxl {
|
|
|
|
namespace {
|
|
|
|
constexpr size_t kScaleWidth = 200;
|
|
|
|
#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 = JXL_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 = reinterpret_cast<GtkAdjustment*>(
|
|
gimp_scale_entry_new(GTK_TABLE(table), 0, 0, "Distance", kScaleWidth, 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(reinterpret_cast<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 = reinterpret_cast<GtkAdjustment*>(gimp_scale_entry_new(
|
|
GTK_TABLE(table), 0, 1, "Quality", kScaleWidth, 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 = reinterpret_cast<GtkAdjustment*>(
|
|
gimp_scale_entry_new(GTK_TABLE(table), 0, 3, "Speed", kScaleWidth, 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
|
|
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 = reinterpret_cast<GtkAdjustment*>(
|
|
gimp_scale_entry_new(GTK_TABLE(table), 0, 0, "Faster Decoding",
|
|
kScaleWidth, 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);
|
|
} // 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 = JxlEncoderDistanceFromQuality(quality);
|
|
|
|
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 =
|
|
static_cast<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 =
|
|
static_cast<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 = JXL_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 = JXL_FALSE;
|
|
jxl_save_opts.lossless = false;
|
|
}
|
|
} else {
|
|
g_printerr(SAVE_PROC " Warning: Using internal profile.\n");
|
|
jxl_save_opts.basic_info.uses_original_profile = JXL_FALSE;
|
|
jxl_save_opts.lossless = false;
|
|
}
|
|
|
|
// set up internal color profile
|
|
JxlColorEncoding color_encoding = {};
|
|
|
|
if (jxl_save_opts.is_linear) {
|
|
JxlColorEncodingSetToLinearSRGB(&color_encoding,
|
|
TO_JXL_BOOL(jxl_save_opts.is_gray));
|
|
} else {
|
|
JxlColorEncodingSetToSRGB(&color_encoding,
|
|
TO_JXL_BOOL(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, JXL_FALSE);
|
|
JxlEncoderSetFrameDistance(frame_settings, 0.01);
|
|
} else {
|
|
JxlEncoderSetFrameDistance(frame_settings, 0);
|
|
JxlEncoderSetFrameLossless(frame_settings, JXL_TRUE);
|
|
}
|
|
} else {
|
|
jxl_save_opts.lossless = false;
|
|
JxlEncoderSetFrameLossless(frame_settings, JXL_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
|