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