UP | HOME

Enhancing document viewer colors

1 The problem

Standard black letters on white background burn eyes out. Inverted colors are not nice, and using xcalib to adjust them is a hack with its own drawbacks. Those are the options that Okular and Evince share, and Okular has some additional options, but I've never been quite satisfied by those. So, an eyes-friendly view mode should be written.

2 Approach

One could think of a bunch of nice working approaches to color alteration, like mapping a few key colors to other ones, and then interpolating that, in some color space, which makes me to wonder even more why wasn't it implemented in any of the viewers I've tried. But the one described here is even more simple: it is to invert luminosity, tweak it a little, and decrease saturation.

More precisely, in HSV:

  1. Invert V: just V = 1 - V
  2. Tweak V: since white on black is still too contrast, V = 0.1 + V * 0.6 (coefficients here, as well as below, are just picked experimentally). Some colors could become less readable after that, and assuming default white background, it's helpful to increase V further, depending on saturation: V = V + S * 0.4.
  3. Decrease saturation: it's both to increase readability (assuming default white background once again), and to obtain soft, pastel colors: S = S * 0.3.

3 Implementation

Here's what I'm using now in Evince:

void
ev_document_misc_invert_surface (cairo_surface_t *surface) {
  int width, height, i;
  unsigned char *data;
  uint32_t pixel;
  uint8_t *d, a, r, g, b;
  double sr, sg, sb, h, s, v, min, max, delta, dr, dg, db, vi, v1, v2, v3;
  if (cairo_image_surface_get_format(surface) == CAIRO_FORMAT_ARGB32) {
    cairo_surface_flush(surface);
    width = cairo_image_surface_get_width(surface);
    height = cairo_image_surface_get_height(surface);
    data = cairo_image_surface_get_data(surface);
    for(i = 0; i < width * height; i++) {
      // get ARGB
      d = &data[i * 4];
      memcpy(&pixel, d, sizeof (uint32_t));
      a = (pixel & 0xff000000) >> 24;
      r = (pixel & 0xff0000) >> 16;
      g = (pixel & 0xff00) >> 8;
      b = pixel & 0xff;
      // RGB -> sRGB
      sr = r / 255.0;
      sg = g / 255.0;
      sb = b / 255.0;
      // sRGB -> HSV
      // http://www.easyrgb.com/index.php?X=MATH&H=20#text20
      min = fmin(sr, fmin(sg, sb));
      max = fmax(sr, fmax(sg, sb));
      delta = max - min;
      v = max;
      if (max == 0.) {
        h = 0.;
        s = 0.;
      } else {
        s = delta / max;
        dr = (((max - sr) / 6.) + (delta / 2.)) / delta;
        dg = (((max - sg) / 6.) + (delta / 2.)) / delta;
        db = (((max - sb) / 6.) + (delta / 2.)) / delta;
        if (sr == max) h = db - dg;
        else if (sg == max) h = (1. / 3.) + dr - db;
        else if (sb == max) h = (2. / 3.) + dg - dr;
        while (h < 0.) h += 1.;
        while (h > 1.) h -= 1.;
      }
      // invert V
      v = 1. - v;
      // exclude strictly black and white colors
      v = .1 + v * .6 + s * .4;
      if (v > 1.) v = 1.;
      s *= .3;
      // HSV -> sRGB
      if (s == 0.) {
        sr = v;
        sg = v;
        sb = v;
      } else {
        h *= 6.;
        if (h == 6.) h = 0.;
        vi = floor(h);
        v1 = v * (1. - s);
        v2 = v * (1. - s * (h - vi));
        v3 = v * (1. - s * (1. - (h - vi)));
        switch ((int)vi) {
        case 0:  sr = v;  sg = v3; sb = v1; break;
        case 1:  sr = v2; sg = v;  sb = v1; break;
        case 2:  sr = v1; sg = v;  sb = v3; break;
        case 3:  sr = v1; sg = v2; sb = v;  break;
        case 4:  sr = v3; sg = v1; sb = v;  break;
        default: sr = v;  sg = v1; sb = v2;
        }
      }
      // sRGB -> RGB
      r = (uint8_t)floor(sr * 255.);
      g = (uint8_t)floor(sg * 255.);
      b = (uint8_t)floor(sb * 255.);
      a = 0;
      // set ARGB
      pixel = (a << 24) | (r << 16) | (g << 8) | b;
      memcpy(d, &pixel, sizeof (uint32_t));
    }
    cairo_surface_mark_dirty(surface);
  }
}

4 Examples

5 One more thing

As one could observe, a font without serifs is much nicer to read on all three screenshots. Apparently fonts are usually designed with black-on-white color theme in mind, what shows up on some more than on others, and very few fonts with serifs are fine even with inverted colors; EB Garamond is among them, and DejaVu are not the worst ones. Though sans serif fonts are still much safer.