[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[nongnu] elpa/pdf-tools fdb187493f 1/2: Add support for midnight mode wi
From: |
ELPA Syncer |
Subject: |
[nongnu] elpa/pdf-tools fdb187493f 1/2: Add support for midnight mode with color |
Date: |
Tue, 17 Jan 2023 01:59:35 -0500 (EST) |
branch: elpa/pdf-tools
commit fdb187493fe6f10fea31a76daa98c07db591cd90
Author: Zach Kost-Smith <zachkostsmith@gmail.com>
Commit: Vedang Manerikar <ved.manerikar@gmail.com>
Add support for midnight mode with color
This inversion method attempts to maintain the color hue and saturation but
inverts the lightness using the OKLab color space[^1].
[^1]: https://bottosson.github.io/posts/oklab/
* server/epdfinfo.c (image-recolor): Add feature to support the OKLab
inversion method functionality
* lisp/pdf-view.el (pdf-view-midnight-invert): Add new variable to
invert the image color lightness while maintaining hue.
(pdf-view-midnight-minor-mode): Account for above.
* lisp/pdf-info.el (pdf-info-query--parse-response): Handle changes to
:render/usecolors command
Closes: #69
Closes: #169
Closes: politza/pdf-tools#698
Closes: politza/pdf-tools#608
---
lisp/pdf-info.el | 23 +++++-
lisp/pdf-view.el | 29 ++++++-
server/epdfinfo.c | 240 +++++++++++++++++++++++++++++++++++++++++++++---------
3 files changed, 248 insertions(+), 44 deletions(-)
diff --git a/lisp/pdf-info.el b/lisp/pdf-info.el
index d3e17bf07d..7cc43882bc 100644
--- a/lisp/pdf-info.el
+++ b/lisp/pdf-info.el
@@ -570,8 +570,14 @@ interrupted."
(let ((key (intern (car key-value)))
(value (cadr key-value)))
(cl-case key
- ((:render/printed :render/usecolors)
- (setq value (equal value "1"))))
+ ((:render/printed)
+ (setq value (equal value "1")))
+ ((:render/usecolors)
+ (setq value (ignore-errors
+ (let ((int-val (cl-parse-integer value)))
+ (if (> int-val 0)
+ int-val
+ nil))))))
(push value options)
(push key options)))
options))
@@ -1726,8 +1732,19 @@ Returns a list \(LEFT TOP RIGHT BOT\)."
((:render/foreground :render/background)
(push (pdf-util-hexcolor value)
soptions))
- ((:render/usecolors :render/printed)
+ ((:render/printed)
(push (if value 1 0) soptions))
+ ((:render/usecolors)
+ ;; 0 -> original color
+ ;; 1 -> recolor document to grayscale mapping black to
+ ;; :render/foreground and white to :render/background
+ ;; 2 -> recolor document by inverting the perceived lightness
+ ;; preserving hue
+ (push (cond ((integerp value) value)
+ ;; Map nil -> 0 and t -> 1
+ (value 1)
+ (t 0))
+ soptions))
(t (push value soptions)))
(push key soptions)))
soptions)))
diff --git a/lisp/pdf-view.el b/lisp/pdf-view.el
index afca46f54e..6197029c19 100644
--- a/lisp/pdf-view.el
+++ b/lisp/pdf-view.el
@@ -118,6 +118,20 @@ This should be a cons \(FOREGROUND . BACKGROUND\) of
colors."
:type '(cons (color :tag "Foreground")
(color :tag "Background")))
+(defcustom pdf-view-midnight-invert nil
+ "In midnight mode invert the image color lightness maintaining hue.
+
+This is particularly useful if you are viewing documents with
+color coded data in plots. This will maintain the colors such
+that 'blue' and 'red' will remain these colors in the inverted
+rendering. This inversion is non-trivial. This makes use of the
+OKLab color space which is well calibrated to have equal
+perceptual brightness across hue, but not all colors are within
+the RGB gamut after inversion, causing some colors to saturate.
+Nevertheless, this seems to work well in most cases."
+ :group 'pdf-view
+ :type 'boolean)
+
(defcustom pdf-view-change-page-hook nil
"Hook run after changing to another page, but before displaying it.
@@ -1243,7 +1257,16 @@ The colors are determined by the variable
(pdf-info-setoptions
:render/foreground (or (car pdf-view-midnight-colors)
"black")
:render/background (or (cdr pdf-view-midnight-colors)
"white")
- :render/usecolors t))))
+ :render/usecolors
+ (if pdf-view-midnight-invert
+ ;; If midnight invert is enabled, pass "2" indicating
+ ;; that :render/foreground and :render/background should
+ ;; be ignored and to instead invert the PDF (preserving
+ ;; hue)
+ 2
+ ;; If invert is not enabled, pass "1" indictating that
+ ;; :render/foreground and :render/background should be
used
+ 1)))))
(cond
(pdf-view-midnight-minor-mode
(add-hook 'after-save-hook enable nil t)
@@ -1252,7 +1275,9 @@ The colors are determined by the variable
(t
(remove-hook 'after-save-hook enable t)
(remove-hook 'after-revert-hook enable t)
- (pdf-info-setoptions :render/usecolors nil))))
+ (pdf-info-setoptions
+ ;; Value "0" indicates that colors should remain unchanged
+ :render/usecolors 0))))
(pdf-cache-clear-images)
(pdf-view-redisplay t))
diff --git a/server/epdfinfo.c b/server/epdfinfo.c
index 32898fa9fc..b82cb6fca7 100644
--- a/server/epdfinfo.c
+++ b/server/epdfinfo.c
@@ -370,16 +370,117 @@ mktempfile()
return filename;
}
+/* Holds RGB, HSL, HSV, Lab, or Lch... but note that the order in memory for
HSL
+ * and HSV are actually VSH and LSH. */
+struct color
+{
+ union
+ {
+ double r, v, l;
+ };
+ union
+ {
+ double g, s, a;
+ };
+ union
+ {
+ double b, h;
+ };
+};
+
+#define struct_color(x) (*((struct color *) x))
+#define vec_color(x) ((double *) &x)
+
+// Using values reported at
https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
+// instead of going through xyz. This ensures any whitepoint is ignored
+static struct color
+rgb2oklab(struct color rgb)
+{
+ struct color srgb;
+
+ for (int i = 0; i < 3; i++)
+ {
+ double val = vec_color(rgb)[i];
+ vec_color(srgb)[i] = ((val > 0.04045)
+ ? pow((val + 0.055) / 1.055, 2.4)
+ : (val / 12.92));
+ }
+
+ double l = 0.4121656120 * srgb.r + 0.5362752080 * srgb.g + 0.0514575653 *
srgb.b;
+ double m = 0.2118591070 * srgb.r + 0.6807189584 * srgb.g + 0.1074065790 *
srgb.b;
+ double s = 0.0883097947 * srgb.r + 0.2818474174 * srgb.g + 0.6302613616 *
srgb.b;
+
+ l = cbrt(l);
+ m = cbrt(m);
+ s = cbrt(s);
+
+ return (struct color) {
+ .l = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
+ .a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
+ .b = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s
+ };
+}
+
+static double clamp(double x, double low, double high)
+{
+ return ((x < low)
+ ? low
+ : ((x > high) ? high : x));
+}
+
+static struct color
+oklab2rgb(struct color lab)
+{
+ double l = lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b;
+ double m = lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b;
+ double s = lab.l - 0.0894841775 * lab.a - 1.2914855480 * lab.b;
+
+ l = l * l * l;
+ m = m * m * m;
+ s = s * s * s;
+
+ struct color srgb = {
+ .r = 4.0767245293 * l - 3.3072168827 * m + 0.2307590544 * s,
+ .g = -1.2681437731 * l + 2.6093323231 * m - 0.3411344290 * s,
+ .b = -0.0041119885 * l - 0.7034763098 * m + 1.7068625689 * s
+ };
+
+ struct color rgb;
+ for (int i = 0; i < 3; i++)
+ {
+ double val = vec_color(srgb)[i];
+ val = ((val > 0.0031308)
+ ? (1.055 * pow(val, 1 / 2.4) - 0.055)
+ : (12.92 * val));
+
+ vec_color(rgb)[i] = clamp(val, 0.0, 1.0);
+ }
+
+ return rgb;
+}
+
+#undef struct_color
+#undef vec_color
+
+static inline gboolean color_equal(struct color a, struct color b)
+{
+ return (a.r == b.r && a.g == b.g && a.b == b.b);
+}
+
static void
image_recolor (cairo_surface_t * surface, const PopplerColor * fg,
- const PopplerColor * bg)
-{
- /* uses a representation of a rgb color as follows:
- - a lightness scalar (between 0,1), which is a weighted average of r, g,
b,
- - a hue vector, which indicates a radian direction from the grey axis,
- inside the equal lightness plane.
- - a saturation scalar between 0,1. It is 0 when grey, 1 when the color is
- in the boundary of the rgb cube.
+ const PopplerColor * bg, int usecolors)
+{
+ /* Performs one of two kinds of image recoloring depending on the value of
usecolors:
+
+ 1 -> uses a representation of a rgb color as follows:
+ - a lightness scalar (between 0,1), which is a weighted average of
r, g, b,
+ - a hue vector, which indicates a radian direction from the grey
axis,
+ inside the equal lightness plane.
+ - a saturation scalar between 0,1. It is 0 when grey, 1 when the
color is
+ in the boundary of the rgb cube.
+
+ 2 -> Invert the perceived lightness in the image while maintaining hue.
*/
const unsigned int page_width = cairo_image_surface_get_width (surface);
@@ -391,19 +492,30 @@ image_recolor (cairo_surface_t * surface, const
PopplerColor * fg,
static const double a[] = { 0.30, 0.59, 0.11 };
const double f = 65535.;
- const double rgb_fg[] = {
- fg->red / f, fg->green / f, fg->blue / f
+ const struct color rgb_fg = {
+ .r = fg->red / f,
+ .g = fg->green / f,
+ .b = fg->blue / f
};
- const double rgb_bg[] = {
- bg->red / f, bg->green / f, bg->blue / f
+ const struct color rgb_bg = {
+ .r = bg->red / f,
+ .g = bg->green / f,
+ .b = bg->blue / f
};
- const double rgb_diff[] = {
- rgb_bg[0] - rgb_fg[0],
- rgb_bg[1] - rgb_fg[1],
- rgb_bg[2] - rgb_fg[2]
+ const struct color rgb_diff = {
+ .r = rgb_bg.r - rgb_fg.r,
+ .g = rgb_bg.g - rgb_fg.g,
+ .b = rgb_bg.b - rgb_fg.b
};
+ /* The Oklab transform is expensive, precompute white->black and have a
single
+ entry cache to speed up computation */
+ struct color white = {.r = 1.0, .g = 1.0, .b = 1.0};
+ struct color black = {.r = 0.0, .g = 0.0, .b = 0.0};
+ struct color precomputed_rgb = white;
+ struct color precomputed_inv_rgb = black;
+
unsigned int y;
for (y = 0; y < page_height * rowstride; y += rowstride)
{
@@ -411,26 +523,76 @@ image_recolor (cairo_surface_t * surface, const
PopplerColor * fg,
unsigned int x;
for (x = 0; x < page_width; x++, data += 4)
- {
- /* Careful. data color components blue, green, red. */
- const double rgb[3] = {
- (double) data[2] / 256.,
- (double) data[1] / 256.,
- (double) data[0] / 256.
- };
-
- /* compute h, s, l data */
- double l = a[0] * rgb[0] + a[1] * rgb[1] + a[2] * rgb[2];
-
- /* linear interpolation between dark and light with color ligtness as
- * a parameter */
- data[2] =
- (unsigned char) round (255. * (l * rgb_diff[0] + rgb_fg[0]));
- data[1] =
- (unsigned char) round (255. * (l * rgb_diff[1] + rgb_fg[1]));
- data[0] =
- (unsigned char) round (255. * (l * rgb_diff[2] + rgb_fg[2]));
- }
+ {
+ /* Careful. data color components blue, green, red. */
+ struct color rgb = {
+ .r = (double) data[2] / 256.,
+ .g = (double) data[1] / 256.,
+ .b = (double) data[0] / 256.
+ };
+
+ switch (usecolors)
+ {
+ case 0:
+ /* No image recoloring requested. Do nothing in this case.
+ Should never be called as we should never call with unless
+ usecolors != 0. */
+ break;
+ case 1:
+ {
+ /* Linear interpolation between bg and fg based on the
+ perceptual lightness measure l */
+ /* compute h, s, l data */
+ double l = a[0] * rgb.r + a[1] * rgb.g + a[2] * rgb.b;
+
+ /* linear interpolation between dark and light with color
+ lightness as a parameter */
+ data[2] =
+ (unsigned char) round (255. * (l * rgb_diff.r + rgb_fg.r));
+ data[1] =
+ (unsigned char) round (255. * (l * rgb_diff.g + rgb_fg.g));
+ data[0] =
+ (unsigned char) round (255. * (l * rgb_diff.b + rgb_fg.b));
+ }
+ break;
+ case 2:
+ {
+ /* Convert to Oklab coordinates, invert perceived lightness,
+ convert back to RGB. */
+ if (color_equal(white, rgb))
+ {
+ rgb = black;
+ }
+ else if (color_equal(precomputed_rgb, rgb))
+ {
+ rgb = precomputed_inv_rgb;
+ }
+ else
+ {
+ struct color oklab = rgb2oklab(rgb);
+ precomputed_rgb = rgb;
+
+ /* Gamma correction. Shouldn't be necessary, but colors
+ * 'feel' too dark and fonts too thin otherwise. */
+ oklab.l = pow(oklab.l, 1.8);
+
+ /* Invert the perceived lightness */
+ oklab.l = 1.0 - oklab.l;
+
+ rgb = oklab2rgb(oklab);
+
+ precomputed_inv_rgb = rgb;
+ }
+
+ data[2] = (unsigned char) round(255. * rgb.r);
+ data[1] = (unsigned char) round(255. * rgb.g);
+ data[0] = (unsigned char) round(255. * rgb.b);
+ }
+ break;
+ default:
+ internal_error ("image_recolor switch fell through");
+ }
+ }
}
}
@@ -501,8 +663,8 @@ image_render_page(PopplerDocument *pdf, PopplerPage *page,
cairo_paint (cr);
- if (options && options->usecolors)
- image_recolor (surface, &options->fg, &options->bg);
+ if (options && (options->usecolors))
+ image_recolor (surface, &options->fg, &options->bg, options->usecolors);
cairo_destroy (cr);
@@ -3444,7 +3606,7 @@ cmd_charlayout(const epdfinfo_t *ctx, const command_arg_t
*args)
const document_option_t document_options [] =
{
- DEC_DOPT (":render/usecolors", ARG_BOOL, render.usecolors),
+ DEC_DOPT (":render/usecolors", ARG_NATNUM, render.usecolors),
DEC_DOPT (":render/printed", ARG_BOOL, render.printed),
DEC_DOPT (":render/foreground", ARG_COLOR, render.fg),
DEC_DOPT (":render/background", ARG_COLOR, render.bg),