Compare commits

...

5 Commits

Author SHA1 Message Date
Přemysl Eric Janouch 3c8a280546
Move colour management to its own compilation unit
Also make it apparent that CMM profiles are pointer types.

This isn't all that pretty, but it's a necessary first step.
2024-01-26 19:17:54 +01:00
Přemysl Eric Janouch 96189b70b8
Mark places where lcms2 should use contexts 2024-01-26 17:25:04 +01:00
Přemysl Eric Janouch 67433f3776
Add a --collection toggle
One possible use of it is to avoid thumbnailing the parent directory.
2024-01-26 16:57:36 +01:00
Přemysl Eric Janouch c1418c7462
Decrease sidebar padding
Nothing fits in there normally, it's about time to acknowledge that.
2024-01-26 16:38:22 +01:00
Přemysl Eric Janouch 935506b120
Make the Delete key move files to trash in browser 2024-01-26 16:37:29 +01:00
11 changed files with 461 additions and 387 deletions

View File

@ -32,6 +32,10 @@ Options
handler to implement the "Open Containing Folder" feature of certain
applications.
*--collection*::
Always put arguments in a virtual directory, even when only one is passed.
Implies *--browse*.
*--help-all*::
Show the full list of options, including those provided by GTK+.

View File

@ -1,7 +1,7 @@
//
// fiv-browser.c: filesystem browsing widget
//
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -1562,6 +1562,14 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
case 0:
switch (event->keyval) {
case GDK_KEY_Delete:
if (self->selected) {
GtkWindow *window = GTK_WINDOW(gtk_widget_get_toplevel(widget));
GFile *file = g_file_new_for_uri(self->selected->e->uri);
fiv_context_menu_remove(window, file);
g_object_unref(file);
}
return GDK_EVENT_STOP;
case GDK_KEY_Return:
if (self->selected)
return open_entry(widget, self->selected, FALSE);

View File

@ -1,7 +1,7 @@
//
// fiv-context-menu.c: popup menu
//
// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -328,17 +328,13 @@ open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
}
static void
open_context_show_error_dialog(OpenContext *self, GError *error)
show_error_dialog(GtkWindow *parent, GError *error)
{
GtkWindow *window = g_weak_ref_get(&self->window);
GtkWidget *dialog =
gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL,
gtk_message_dialog_new(GTK_WINDOW(parent), GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
g_clear_object(&window);
g_error_free(error);
}
@ -357,7 +353,9 @@ open_context_launch(GtkWidget *widget, OpenContext *self)
(void) g_app_info_set_as_last_used_for_type(
self->app_info, self->content_type, NULL);
} else {
open_context_show_error_dialog(self, error);
GtkWindow *window = g_weak_ref_get(&self->window);
show_error_dialog(window, error);
g_clear_object(&window);
}
g_list_free(files);
g_object_unref(context);
@ -437,14 +435,22 @@ on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
g_free(uri);
}
void
fiv_context_menu_remove(GtkWindow *parent, GFile *file)
{
// TODO(p): Use g_file_trash_async(), for which we need a task manager.
GError *error = NULL;
if (!g_file_trash(file, NULL, &error))
show_error_dialog(parent, error);
}
static void
on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
{
// TODO(p): Use g_file_trash_async(), for which we need a task manager.
OpenContext *ctx = user_data;
GError *error = NULL;
if (!g_file_trash(ctx->file, NULL, &error))
open_context_show_error_dialog(ctx, error);
GtkWindow *window = g_weak_ref_get(&ctx->window);
fiv_context_menu_remove(window, ctx->file);
g_clear_object(&window);
}
static gboolean

View File

@ -1,7 +1,7 @@
//
// fiv-context-menu.h: popup menu
//
// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -18,4 +18,5 @@
#include <gtk/gtk.h>
void fiv_context_menu_information(GtkWindow *parent, const char *uri);
void fiv_context_menu_remove(GtkWindow *parent, GFile *file);
GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);

366
fiv-io-profile.c Normal file
View File

@ -0,0 +1,366 @@
//
// fiv-profile.c: colour management
//
// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include "config.h"
#include <glib.h>
#include <stdbool.h>
#include "fiv-io.h"
// Colour management must be handled before RGB conversions.
#ifdef HAVE_LCMS2
#include <lcms2.h>
#endif // HAVE_LCMS2
#ifdef HAVE_LCMS2_FAST_FLOAT
#include <lcms2_fast_float.h>
#endif // HAVE_LCMS2_FAST_FLOAT
void
fiv_io_profile_init(void)
{
// TODO(p): Use Little CMS with contexts instead.
#ifdef HAVE_LCMS2_FAST_FLOAT
cmsPluginTHR(NULL, cmsFastFloatExtensions());
#endif // HAVE_LCMS2_FAST_FLOAT
}
FivIoProfile *
fiv_io_profile_new(const void *data, size_t len)
{
#ifdef HAVE_LCMS2
return cmsOpenProfileFromMemTHR(NULL, data, len);
#else
(void) data;
(void) len;
return NULL;
#endif
}
FivIoProfile *
fiv_io_profile_new_sRGB(void)
{
#ifdef HAVE_LCMS2
return cmsCreate_sRGBProfileTHR(NULL);
#else
return NULL;
#endif
}
FivIoProfile *
fiv_io_profile_new_parametric(
double gamma, double whitepoint[2], double primaries[6])
{
#ifdef HAVE_LCMS2
// TODO(p): Make sure to use the library in a thread-safe manner.
cmsContext context = NULL;
const cmsCIExyY cmsWP = {whitepoint[0], whitepoint[1], 1.0};
const cmsCIExyYTRIPLE cmsP = {
{primaries[0], primaries[1], 1.0},
{primaries[2], primaries[3], 1.0},
{primaries[4], primaries[5], 1.0},
};
cmsToneCurve *curve = cmsBuildGamma(context, gamma);
if (!curve)
return NULL;
cmsHPROFILE profile = cmsCreateRGBProfileTHR(
context, &cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve});
cmsFreeToneCurve(curve);
return profile;
#else
(void) gamma;
(void) whitepoint;
(void) primaries;
return NULL;
#endif
}
FivIoProfile *
fiv_io_profile_new_sRGB_gamma(double gamma)
{
return fiv_io_profile_new_parametric(gamma,
(double[2]){0.3127, 0.3290},
(double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600});
}
FivIoProfile *
fiv_io_profile_new_from_bytes(GBytes *bytes)
{
gsize len = 0;
gconstpointer p = g_bytes_get_data(bytes, &len);
return fiv_io_profile_new(p, len);
}
GBytes *
fiv_io_profile_to_bytes(FivIoProfile *profile)
{
#ifdef HAVE_LCMS2
cmsUInt32Number len = 0;
(void) cmsSaveProfileToMem(profile, NULL, &len);
gchar *data = g_malloc0(len);
if (!cmsSaveProfileToMem(profile, data, &len)) {
g_free(data);
return NULL;
}
return g_bytes_new_take(data, len);
#else
(void) profile;
return NULL;
#endif
}
void
fiv_io_profile_free(FivIoProfile *self)
{
#ifdef HAVE_LCMS2
cmsCloseProfile(self);
#else
(void) self;
#endif
}
// --- Image loading -----------------------------------------------------------
// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
#ifndef HAVE_LCMS2
#define FIV_IO_PROFILE_ARGB32 0
#define FIV_IO_PROFILE_4X16LE 0
#else
#define FIV_IO_PROFILE_ARGB32 \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
#define FIV_IO_PROFILE_4X16LE \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
#endif
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
// ARGB/BGRA/XRGB/BGRX.
static void
trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
{
// This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms.
// It will typically produce horribly oversaturated results.
// Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop
// does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096
while (len--) {
int c = p[0], m = p[1], y = p[2], k = p[3];
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
p[0] = k * y / 255;
p[1] = k * m / 255;
p[2] = k * c / 255;
p[3] = 255;
#else
p[3] = k * y / 255;
p[2] = k * m / 255;
p[1] = k * c / 255;
p[0] = 255;
#endif
p += 4;
}
}
void
fiv_io_profile_cmyk(
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
#ifndef HAVE_LCMS2
(void) source;
(void) target;
#else
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransformTHR(NULL, source, TYPE_CMYK_8_REV, target,
FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(
transform, image->data, image->data, image->width * image->height);
cmsDeleteTransform(transform);
return;
}
#endif
trivial_cmyk_to_host_byte_order_argb(
image->data, image->width * image->height);
}
static bool
fiv_io_profile_rgb_direct(unsigned char *data, int w, int h,
FivIoProfile *source, FivIoProfile *target,
uint32_t source_format, uint32_t target_format)
{
#ifndef HAVE_LCMS2
(void) data;
(void) w;
(void) h;
(void) source;
(void) source_format;
(void) target;
(void) target_format;
return false;
#else
// TODO(p): We should make this optional.
cmsHPROFILE src_fallback = NULL;
if (target && !source)
source = src_fallback = cmsCreate_sRGBProfileTHR(NULL);
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransformTHR(NULL,
source, source_format, target, target_format, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(transform, data, data, w * h);
cmsDeleteTransform(transform);
}
if (src_fallback)
cmsCloseProfile(src_fallback);
return transform != NULL;
#endif
}
static void
fiv_io_profile_xrgb32(
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
fiv_io_profile_rgb_direct(image->data, image->width, image->height,
source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
}
void
fiv_io_profile_4x16le_direct(unsigned char *data,
int w, int h, FivIoProfile *source, FivIoProfile *target)
{
fiv_io_profile_rgb_direct(data, w, h, source, target,
FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void
fiv_io_profile_page(FivIoImage *page, FivIoProfile *target,
void (*frame_cb) (FivIoImage *, FivIoProfile *, FivIoProfile *))
{
FivIoProfile *source = NULL;
if (page->icc)
source = fiv_io_profile_new_from_bytes(page->icc);
// TODO(p): All animations need to be composited in a linear colour space.
for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
frame_cb(frame, source, target);
if (source)
fiv_io_profile_free(source);
}
// From libwebp, verified to exactly match [x * a / 255].
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
void
fiv_io_premultiply_argb32(FivIoImage *image)
{
if (image->format != CAIRO_FORMAT_ARGB32)
return;
for (uint32_t y = 0; y < image->height; y++) {
uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
for (uint32_t x = 0; x < image->width; x++) {
uint32_t argb = dstp[x], a = argb >> 24;
dstp[x] = a << 24 |
PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 |
PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 |
PREMULTIPLY8(a, 0xFF & argb);
}
}
}
#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
#define FIV_IO_PROFILE_ARGB32_PREMUL \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
static void
fiv_io_profile_argb32(FivIoImage *image,
FivIoProfile *source, FivIoProfile *target)
{
g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
fiv_io_profile_rgb_direct(image->data, image->width, image->height,
source, target,
FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
}
void
fiv_io_profile_argb32_premultiply(
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
if (image->format != CAIRO_FORMAT_ARGB32) {
fiv_io_profile_xrgb32(image, source, target);
} else if (!fiv_io_profile_rgb_direct(image->data,
image->width, image->height, source, target,
FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
g_debug("failed to create a premultiplying transform");
fiv_io_premultiply_argb32(image);
}
}
#else // ! HAVE_LCMS2 || LCMS_VERSION < 2130
// TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
#define fiv_io_profile_argb32(surface, source, target)
static void
fiv_io_profile_argb32_premultiply(
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
fiv_io_profile_xrgb32(image, source, target);
fiv_io_premultiply_argb32(image);
}
#endif // ! HAVE_LCMS2 || LCMS_VERSION < 2130
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void
fiv_io_profile_any(
FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
{
// TODO(p): Ensure we do colour management early enough, so that
// no avoidable increase of quantization error occurs beforehands,
// and also for correct alpha compositing.
switch (image->format) {
break; case CAIRO_FORMAT_RGB24:
fiv_io_profile_xrgb32(image, source, target);
break; case CAIRO_FORMAT_ARGB32:
fiv_io_profile_argb32(image, source, target);
break; default:
g_debug("CM attempted on an unsupported surface format");
}
}
// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
FivIoImage *
fiv_io_profile_finalize(FivIoImage *image, FivIoProfile *target)
{
if (!target)
return image;
for (FivIoImage *page = image; page != NULL; page = page->page_next)
fiv_io_profile_page(page, target, fiv_io_profile_any);
return image;
}

350
fiv-io.c
View File

@ -34,11 +34,6 @@
#include <libjpegqs.h>
#endif // HAVE_JPEG_QS
// Colour management must be handled before RGB conversions.
#ifdef HAVE_LCMS2
#include <lcms2.h>
#endif // HAVE_LCMS2
#define TIFF_TABLES_CONSTANTS_ONLY
#include "tiff-tables.h"
#include "tiffer.h"
@ -291,333 +286,6 @@ try_append_page(
return true;
}
// --- Colour management -------------------------------------------------------
FivIoProfile
fiv_io_profile_new(const void *data, size_t len)
{
#ifdef HAVE_LCMS2
return cmsOpenProfileFromMem(data, len);
#else
(void) data;
(void) len;
return NULL;
#endif
}
FivIoProfile
fiv_io_profile_new_sRGB(void)
{
#ifdef HAVE_LCMS2
return cmsCreate_sRGBProfile();
#else
return NULL;
#endif
}
FivIoProfile
fiv_io_profile_new_parametric(
double gamma, double whitepoint[2], double primaries[6])
{
#ifdef HAVE_LCMS2
// TODO(p): Make sure to use the library in a thread-safe manner.
cmsContext context = NULL;
const cmsCIExyY cmsWP = {whitepoint[0], whitepoint[1], 1.0};
const cmsCIExyYTRIPLE cmsP = {
{primaries[0], primaries[1], 1.0},
{primaries[2], primaries[3], 1.0},
{primaries[4], primaries[5], 1.0},
};
cmsToneCurve *curve = cmsBuildGamma(context, gamma);
if (!curve)
return NULL;
cmsHPROFILE profile = cmsCreateRGBProfileTHR(
context, &cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve});
cmsFreeToneCurve(curve);
return profile;
#else
(void) gamma;
(void) whitepoint;
(void) primaries;
return NULL;
#endif
}
FivIoProfile
fiv_io_profile_new_sRGB_gamma(double gamma)
{
return fiv_io_profile_new_parametric(gamma,
(double[2]){0.3127, 0.3290},
(double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600});
}
static FivIoProfile
fiv_io_profile_new_from_bytes(GBytes *bytes)
{
gsize len = 0;
gconstpointer p = g_bytes_get_data(bytes, &len);
return fiv_io_profile_new(p, len);
}
static GBytes *
fiv_io_profile_to_bytes(FivIoProfile profile)
{
#ifdef HAVE_LCMS2
cmsUInt32Number len = 0;
(void) cmsSaveProfileToMem(profile, NULL, &len);
gchar *data = g_malloc0(len);
if (!cmsSaveProfileToMem(profile, data, &len)) {
g_free(data);
return NULL;
}
return g_bytes_new_take(data, len);
#else
(void) profile;
return NULL;
#endif
}
void
fiv_io_profile_free(FivIoProfile self)
{
#ifdef HAVE_LCMS2
cmsCloseProfile(self);
#else
(void) self;
#endif
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
#ifndef HAVE_LCMS2
#define FIV_IO_PROFILE_ARGB32 0
#define FIV_IO_PROFILE_4X16LE 0
#else
#define FIV_IO_PROFILE_ARGB32 \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
#define FIV_IO_PROFILE_4X16LE \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
#endif
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
// ARGB/BGRA/XRGB/BGRX.
static void
trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
{
// This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms.
// It will typically produce horribly oversaturated results.
// Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop
// does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096
while (len--) {
int c = p[0], m = p[1], y = p[2], k = p[3];
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
p[0] = k * y / 255;
p[1] = k * m / 255;
p[2] = k * c / 255;
p[3] = 255;
#else
p[3] = k * y / 255;
p[2] = k * m / 255;
p[1] = k * c / 255;
p[0] = 255;
#endif
p += 4;
}
}
static void
fiv_io_profile_cmyk(
FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
#ifndef HAVE_LCMS2
(void) source;
(void) target;
#else
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransform(source, TYPE_CMYK_8_REV, target,
FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(
transform, image->data, image->data, image->width * image->height);
cmsDeleteTransform(transform);
return;
}
#endif
trivial_cmyk_to_host_byte_order_argb(
image->data, image->width * image->height);
}
static bool
fiv_io_profile_rgb_direct(unsigned char *data, int w, int h,
FivIoProfile source, FivIoProfile target,
uint32_t source_format, uint32_t target_format)
{
#ifndef HAVE_LCMS2
(void) data;
(void) w;
(void) h;
(void) source;
(void) source_format;
(void) target;
(void) target_format;
return false;
#else
// TODO(p): We should make this optional.
cmsHPROFILE src_fallback = NULL;
if (target && !source)
source = src_fallback = cmsCreate_sRGBProfile();
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransform(
source, source_format, target, target_format, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(transform, data, data, w * h);
cmsDeleteTransform(transform);
}
if (src_fallback)
cmsCloseProfile(src_fallback);
return transform != NULL;
#endif
}
static void
fiv_io_profile_xrgb32(
FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
fiv_io_profile_rgb_direct(image->data, image->width, image->height,
source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
}
static void
fiv_io_profile_4x16le_direct(
unsigned char *data, int w, int h, FivIoProfile source, FivIoProfile target)
{
fiv_io_profile_rgb_direct(data, w, h, source, target,
FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
fiv_io_profile_page(FivIoImage *page, FivIoProfile target,
void (*frame_cb) (FivIoImage *, FivIoProfile, FivIoProfile))
{
FivIoProfile source = NULL;
if (page->icc)
source = fiv_io_profile_new_from_bytes(page->icc);
// TODO(p): All animations need to be composited in a linear colour space.
for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
frame_cb(frame, source, target);
if (source)
fiv_io_profile_free(source);
}
static void
fiv_io_premultiply_argb32(FivIoImage *image)
{
if (image->format != CAIRO_FORMAT_ARGB32)
return;
for (uint32_t y = 0; y < image->height; y++) {
uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
for (uint32_t x = 0; x < image->width; x++) {
uint32_t argb = dstp[x], a = argb >> 24;
dstp[x] = a << 24 |
PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 |
PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 |
PREMULTIPLY8(a, 0xFF & argb);
}
}
}
#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
#define FIV_IO_PROFILE_ARGB32_PREMUL \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
static void
fiv_io_profile_argb32(FivIoImage *image,
FivIoProfile source, FivIoProfile target)
{
g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
fiv_io_profile_rgb_direct(image->data, image->width, image->height,
source, target,
FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
}
static void
fiv_io_profile_argb32_premultiply(
FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
if (image->format != CAIRO_FORMAT_ARGB32) {
fiv_io_profile_xrgb32(image, source, target);
} else if (!fiv_io_profile_rgb_direct(image->data,
image->width, image->height, source, target,
FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
g_debug("failed to create a premultiplying transform");
fiv_io_premultiply_argb32(image);
}
}
#else // ! HAVE_LCMS2 || LCMS_VERSION < 2130
// TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
#define fiv_io_profile_argb32(surface, source, target)
static void
fiv_io_profile_argb32_premultiply(
FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
fiv_io_profile_xrgb32(image, source, target);
fiv_io_premultiply_argb32(image);
}
#endif // ! HAVE_LCMS2 || LCMS_VERSION < 2130
#define fiv_io_profile_argb32_premultiply_page(page, target) \
fiv_io_profile_page((page), (target), fiv_io_profile_argb32_premultiply)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
fiv_io_profile_any(FivIoImage *image, FivIoProfile source, FivIoProfile target)
{
// TODO(p): Ensure we do colour management early enough, so that
// no avoidable increase of quantization error occurs beforehands,
// and also for correct alpha compositing.
switch (image->format) {
break; case CAIRO_FORMAT_RGB24:
fiv_io_profile_xrgb32(image, source, target);
break; case CAIRO_FORMAT_ARGB32:
fiv_io_profile_argb32(image, source, target);
break; default:
g_debug("CM attempted on an unsupported surface format");
}
}
// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
static FivIoImage *
fiv_io_profile_finalize(FivIoImage *image, FivIoProfile target)
{
if (!target)
return image;
for (FivIoImage *page = image; page != NULL; page = page->page_next)
fiv_io_profile_page(page, target, fiv_io_profile_any);
return image;
}
// --- Wuffs -------------------------------------------------------------------
static bool
@ -711,8 +379,8 @@ struct load_wuffs_frame_context {
GBytes *meta_iccp; ///< Reference-counted ICC profile
GBytes *meta_xmp; ///< Reference-counted XMP
FivIoProfile target; ///< Target device profile, if any
FivIoProfile source; ///< Source colour profile, if any
FivIoProfile *target; ///< Target device profile, if any
FivIoProfile *source; ///< Source colour profile, if any
FivIoImage *result; ///< The resulting image (referenced)
FivIoImage *result_tail; ///< The final animation frame
@ -1390,7 +1058,7 @@ parse_exif_profile_subifd(
}
}
static FivIoProfile
static FivIoProfile *
parse_exif_profile(const void *data, size_t len)
{
struct tiffer T = {};
@ -1586,7 +1254,7 @@ load_jpeg_finalize(FivIoImage *image, bool cmyk,
else
g_byte_array_free(meta.icc, TRUE);
FivIoProfile source = NULL;
FivIoProfile *source = NULL;
if (icc_profile)
source = fiv_io_profile_new(
g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile));
@ -2403,7 +2071,7 @@ load_resvg_destroy(FivIoRenderClosure *closure)
static FivIoImage *
load_resvg_render_internal(FivIoRenderClosureResvg *self,
double scale, FivIoProfile target, GError **error)
double scale, FivIoProfile *target, GError **error)
{
double w = ceil(self->width * scale), h = ceil(self->height * scale);
if (w > SHRT_MAX || h > SHRT_MAX) {
@ -2438,7 +2106,7 @@ load_resvg_render_internal(FivIoRenderClosureResvg *self,
static FivIoImage *
load_resvg_render(
FivIoRenderClosure *closure, FivIoProfile target, double scale)
FivIoRenderClosure *closure, FivIoProfile *target, double scale)
{
FivIoRenderClosureResvg *self = (FivIoRenderClosureResvg *) closure;
return load_resvg_render_internal(self, scale, target, NULL);
@ -2530,7 +2198,7 @@ load_librsvg_destroy(FivIoRenderClosure *closure)
static FivIoImage *
load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
FivIoProfile target, GError **error)
FivIoProfile *target, GError **error)
{
RsvgRectangle viewport = {.x = 0, .y = 0,
.width = self->width * scale, .height = self->height * scale};
@ -2562,7 +2230,7 @@ load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale,
static FivIoImage *
load_librsvg_render(
FivIoRenderClosure *closure, FivIoProfile target, double scale)
FivIoRenderClosure *closure, FivIoProfile *target, double scale)
{
FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure;
return load_librsvg_render_internal(self, scale, target, NULL);
@ -3712,7 +3380,7 @@ set_metadata(WebPMux *mux, const char *fourcc, GBytes *data)
}
gboolean
fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile target,
fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile *target,
const char *path, GError **error)
{
g_return_val_if_fail(page != NULL, FALSE);

View File

@ -1,7 +1,7 @@
//
// fiv-io.h: image operations
//
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -22,16 +22,44 @@
#include <glib.h>
#include <webp/encode.h> // WebPConfig
typedef enum _FivIoOrientation FivIoOrientation;
typedef struct _FivIoRenderClosure FivIoRenderClosure;
typedef struct _FivIoImage FivIoImage;
// --- Colour management -------------------------------------------------------
void fiv_io_profile_init(void);
// TODO(p): Make it also possible to use Skia's skcms.
typedef void *FivIoProfile;
FivIoProfile fiv_io_profile_new(const void *data, size_t len);
FivIoProfile fiv_io_profile_new_sRGB(void);
void fiv_io_profile_free(FivIoProfile self);
FivIoProfile *fiv_io_profile_new(const void *data, size_t len);
FivIoProfile *fiv_io_profile_new_from_bytes(GBytes *bytes);
FivIoProfile *fiv_io_profile_new_sRGB(void);
FivIoProfile *fiv_io_profile_new_sRGB_gamma(double gamma);
FivIoProfile *fiv_io_profile_new_parametric(
double gamma, double whitepoint[2], double primaries[6]);
GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
void fiv_io_profile_free(FivIoProfile *self);
// From libwebp, verified to exactly match [x * a / 255].
#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void fiv_io_premultiply_argb32(FivIoImage *image);
void fiv_io_profile_cmyk(
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
void fiv_io_profile_4x16le_direct(unsigned char *data,
int w, int h, FivIoProfile *source, FivIoProfile *target);
void fiv_io_profile_argb32_premultiply(
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
void fiv_io_profile_page(FivIoImage *page, FivIoProfile *target,
void (*frame_cb) (FivIoImage *, FivIoProfile *, FivIoProfile *));
#define fiv_io_profile_argb32_premultiply_page(page, target) \
fiv_io_profile_page((page), (target), fiv_io_profile_argb32_premultiply)
void fiv_io_profile_any(
FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
FivIoImage *fiv_io_profile_finalize(FivIoImage *image, FivIoProfile *target);
// --- Loading -----------------------------------------------------------------
@ -39,10 +67,6 @@ extern const char *fiv_io_supported_media_types[];
gchar **fiv_io_all_supported_media_types(void);
typedef enum _FivIoOrientation FivIoOrientation;
typedef struct _FivIoRenderClosure FivIoRenderClosure;
typedef struct _FivIoImage FivIoImage;
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
enum _FivIoOrientation {
FivIoOrientationUnknown = 0,
@ -60,7 +84,7 @@ enum _FivIoOrientation {
// then loaders could store it in their closures.
struct _FivIoRenderClosure {
/// The rendering is allowed to fail, returning NULL.
FivIoImage *(*render)(FivIoRenderClosure *, FivIoProfile, double scale);
FivIoImage *(*render)(FivIoRenderClosure *, FivIoProfile *, double scale);
void (*destroy)(FivIoRenderClosure *);
};
@ -179,4 +203,4 @@ unsigned char *fiv_io_encode_webp(
/// Saves the page as a lossless WebP still picture or animation.
/// If no exact frame is specified, this potentially creates an animation.
gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
FivIoProfile target, const char *path, GError **error);
FivIoProfile *target, const char *path, GError **error);

View File

@ -182,7 +182,7 @@ adjust_thumbnail(FivIoImage *thumbnail, double row_height)
FivIoRenderClosure *closure = thumbnail->render;
if (closure && orientation <= FivIoOrientation0) {
// Remember to synchronize changes with render().
FivIoProfile screen_profile = fiv_io_profile_new_sRGB();
FivIoProfile *screen_profile = fiv_io_profile_new_sRGB();
// This API doesn't accept non-uniform scaling; prefer a vertical fit.
FivIoImage *scaled = closure->render(closure, screen_profile, scale_y);
if (screen_profile)

View File

@ -78,7 +78,7 @@ struct _FivView {
double drag_start[2]; ///< Adjustment values for drag origin
FivIoImage *enhance_swap; ///< Quick swap in/out
FivIoProfile screen_cms_profile; ///< Target colour profile for widget
FivIoProfile *screen_cms_profile; ///< Target colour profile for widget
int remaining_loops; ///< Greater than zero if limited
gint64 frame_time; ///< Current frame's start, µs precision
@ -461,7 +461,7 @@ out:
//
// Note that Wayland does not have any appropriate protocol, as of writing:
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
static FivIoProfile
static FivIoProfile *
monitor_cms_profile(GdkWindow *root, int num)
{
char atom[32] = "";
@ -471,7 +471,7 @@ monitor_cms_profile(GdkWindow *root, int num)
int format = 0, length = 0;
GdkAtom type = GDK_NONE;
guchar *data = NULL;
FivIoProfile result = NULL;
FivIoProfile *result = NULL;
if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
if (format == 8 && length > 0)
@ -1117,7 +1117,7 @@ static gboolean
save_as(FivView *self, FivIoImage *frame)
{
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
FivIoProfile target = NULL;
FivIoProfile *target = NULL;
if (self->enable_cms && (target = self->screen_cms_profile)) {
GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",

24
fiv.c
View File

@ -1,7 +1,7 @@
//
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
//
// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@ -44,11 +44,6 @@
#include "fiv-thumbnail.h"
#include "fiv-view.h"
#ifdef HAVE_LCMS2_FAST_FLOAT
#include <lcms2.h>
#include <lcms2_fast_float.h>
#endif // HAVE_LCMS2_FAST_FLOAT
// --- Utilities ---------------------------------------------------------------
static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
@ -2211,6 +2206,7 @@ static const char stylesheet[] = "@define-color fiv-tile @content_view_bg; \
mix(@theme_selected_bg_color, @content_view_bg, 0.5); \
fiv-view, fiv-browser { background: @content_view_bg; } \
placessidebar.fiv box > separator { margin: 4px 0; } \
placessidebar.fiv row { min-height: 1.75em; } \
.fiv-toolbar button { padding-left: 0; padding-right: 0; } \
.fiv-toolbar > button:first-child { padding-left: 4px; } \
.fiv-toolbar > button:last-child { padding-right: 4px; } \
@ -2452,7 +2448,7 @@ on_app_startup(GApplication *app, G_GNUC_UNUSED gpointer user_data)
}
static struct {
gboolean browse, extract_thumbnail;
gboolean browse, collection, extract_thumbnail;
gchar **args, *thumbnail_size, *thumbnail_size_search;
} o;
@ -2462,12 +2458,12 @@ on_app_activate(
{
// XXX: We follow the behaviour of Firefox and Eye of GNOME, which both
// interpret multiple command line arguments differently, as a collection.
// However, single-element collections are unrepresentable this way.
// Should we allow multiple targets only in a special new mode?
// However, single-element collections are unrepresentable this way,
// so we have a switch to enforce it.
g.files_index = -1;
if (o.args) {
const gchar *target = *o.args;
if (o.args[1]) {
if (o.args[1] || o.collection) {
fiv_collection_reload(o.args);
target = FIV_COLLECTION_SCHEME ":/";
}
@ -2583,10 +2579,7 @@ on_app_handle_local_options(G_GNUC_UNUSED GApplication *app,
return 0;
}
// TODO(p): Use Little CMS with contexts instead.
#ifdef HAVE_LCMS2_FAST_FLOAT
cmsPlugin(cmsFastFloatExtensions());
#endif // HAVE_LCMS2_FAST_FLOAT
fiv_io_profile_init();
// Normalize all arguments to URIs, and run thumbnailing modes first.
for (gsize i = 0; o.args && o.args[i]; i++) {
@ -2618,6 +2611,9 @@ main(int argc, char *argv[])
{"browse", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &o.browse,
"Start in filesystem browsing mode", NULL},
{"collection", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, &o.collection,
"Always put arguments in a collection (implies --browse)", NULL},
{"invalidate-cache", 0, G_OPTION_FLAG_IN_MAIN,
G_OPTION_ARG_NONE, NULL,
"Invalidate the wide thumbnail cache", NULL},

View File

@ -164,7 +164,8 @@ tiff_tables = custom_target('tiff-tables.h',
)
desktops = ['fiv.desktop', 'fiv-browse.desktop']
iolib = static_library('fiv-io', 'fiv-io.c', 'xdg.c', tiff_tables,
iolib = static_library('fiv-io', 'fiv-io.c', 'fiv-io-profile.c', 'xdg.c',
tiff_tables,
dependencies : dependencies).extract_all_objects(recursive : true)
exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',