emacs-elpa-diffs
[Top][All Lists]
Advanced

[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),



reply via email to

[Prev in Thread] Current Thread [Next in Thread]