[okular] /: Add some color modes: Invert Lightness/Luma, Hue Shift

Albert Astals Cid null at kde.org
Sat Mar 28 19:13:44 GMT 2020


Git commit aa6833483e9a87b151e38dd82b695ea9f5ce7b8d by Albert Astals Cid, on behalf of David Hurka.
Committed on 28/03/2020 at 19:13.
Pushed by aacid into branch 'master'.

Add some color modes: Invert Lightness/Luma, Hue Shift

M  +5    -0    conf/dlgaccessibility.cpp
M  +40   -0    conf/dlgaccessibilitybase.ui
M  +5    -0    conf/okular_core.kcfg
M  +41   -0    doc/index.docbook
M  +239  -21   ui/pagepainter.cpp
M  +21   -0    ui/pagepainter.h

https://invent.kde.org/kde/okular/commit/aa6833483e9a87b151e38dd82b695ea9f5ce7b8d

diff --git a/conf/dlgaccessibility.cpp b/conf/dlgaccessibility.cpp
index 86532fb56..950b28353 100644
--- a/conf/dlgaccessibility.cpp
+++ b/conf/dlgaccessibility.cpp
@@ -30,6 +30,11 @@ DlgAccessibility::DlgAccessibility( QWidget * parent )
     m_color_pages.append( m_dlg->page_paperColor );
     m_color_pages.append( m_dlg->page_darkLight );
     m_color_pages.append( m_dlg->page_bw );
+    m_color_pages.append( m_dlg->page_invertLightness );
+    m_color_pages.append( m_dlg->page_invertLuma );
+    m_color_pages.append( m_dlg->page_invertLumaSymmetric );
+    m_color_pages.append( m_dlg->page_hueShiftPositive );
+    m_color_pages.append( m_dlg->page_hueShiftNegative );
     for ( QWidget *page : qAsConst(m_color_pages) ) {
         page->hide();
     }
diff --git a/conf/dlgaccessibilitybase.ui b/conf/dlgaccessibilitybase.ui
index 0b5f031d5..d5dd10754 100644
--- a/conf/dlgaccessibilitybase.ui
+++ b/conf/dlgaccessibilitybase.ui
@@ -107,6 +107,31 @@
             <string>Convert to Black & White</string>
            </property>
           </item>
+          <item>
+           <property name="text" >
+            <string>Invert Lightness</string>
+           </property>
+          </item>
+          <item>
+           <property name="text" >
+            <string>Invert Luma (sRGB Linear)</string>
+           </property>
+          </item>
+          <item>
+           <property name="text" >
+            <string>Invert Luma (Symmetric)</string>
+           </property>
+          </item>
+          <item>
+           <property name="text" >
+            <string>Shift Hue Positive</string>
+           </property>
+          </item>
+          <item>
+           <property name="text" >
+            <string>Shift Hue Negative</string>
+           </property>
+          </item>
          </widget>
         </item>
        </layout>
@@ -363,6 +388,21 @@
         </layout>
        </widget>
       </item>
+      <item>
+       <widget class="QWidget" native="1" name="page_invertLightness" />
+      </item>
+      <item>
+       <widget class="QWidget" native="1" name="page_invertLuma" />
+      </item>
+      <item>
+       <widget class="QWidget" native="1" name="page_invertLumaSymmetric" />
+      </item>
+      <item>
+       <widget class="QWidget" native="1" name="page_hueShiftPositive" />
+      </item>
+      <item>
+       <widget class="QWidget" native="1" name="page_hueShiftNegative" />
+      </item>
      </layout>
     </widget>
    </item>
diff --git a/conf/okular_core.kcfg b/conf/okular_core.kcfg
index 267621f06..41b03bd0e 100644
--- a/conf/okular_core.kcfg
+++ b/conf/okular_core.kcfg
@@ -54,6 +54,11 @@
     <choice name="Paper" />
     <choice name="Recolor" />
     <choice name="BlackWhite" />
+    <choice name="InvertLightness" />
+    <choice name="InvertLuma" />
+    <choice name="InvertLumaSymmetric" />
+    <choice name="HueShiftPositive" />
+    <choice name="HueShiftNegative" />
    </choices>
   </entry>
  </group>
diff --git a/doc/index.docbook b/doc/index.docbook
index 4c3a733b7..d7c1defea 100644
--- a/doc/index.docbook
+++ b/doc/index.docbook
@@ -2334,6 +2334,47 @@ Context menu actions like Rename Bookmarks etc.)
 							by moving it to the right will result in lighter grays used.</para>
 					</listitem>
 				</varlistentry>
+				<varlistentry>
+					<term><guilabel>Invert Lightness</guilabel></term>
+					<listitem>
+						<para><action>Inverts</action> lightness of all colors.
+							Light and dark colors will be swapped, but hue and saturation will not be affected.
+							The Contrast in images will usually be worse than in <guilabel>Invert Luma (sRGB Linear)</guilabel>.</para>
+					</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term><guilabel>Invert Luma (sRGB Linear)</guilabel></term>
+					<listitem>
+						<para><action>Inverts</action> luma of all colors.
+							Light and dark will be swapped, but hue and saturation will not be affected.
+							The Contrast in images is preserved better than in <guilabel>Invert Lightness</guilabel>,
+							but graphics and colorful text markup usually look worse.
+							Uses sRGB luma coefficients, but no gamma correction.</para>
+					</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term><guilabel>Invert Luma (Symmetric)</guilabel></term>
+					<listitem>
+						<para><action>Inverts</action> luma of all colors, using symmetric luma coefficients.
+							Light and dark will be swapped, but hue and saturation will not be affected.
+							Very similar to <guilabel>Invert Lightness</guilabel>,
+							but the contrast is in some cases better.</para>
+					</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term><guilabel>Shift Hue Positive</guilabel></term>
+					<listitem>
+						<para><action>Shifts</action> hue of all colors by 120 degrees.
+							Can mitigate some contrast problems in colorful graphics</para>
+					</listitem>
+				</varlistentry>
+				<varlistentry>
+					<term><guilabel>Shift Hue Negative</guilabel></term>
+					<listitem>
+						<para><action>Shifts</action> hue of all colors by 240 degrees.
+							Can mitigate some contrast problems in colorful graphics</para>
+					</listitem>
+				</varlistentry>
 				<varlistentry>
 					<term><guilabel>Engine</guilabel></term>
 					<listitem>
diff --git a/ui/pagepainter.cpp b/ui/pagepainter.cpp
index a6ddb7b1b..19ed71d3d 100644
--- a/ui/pagepainter.cpp
+++ b/ui/pagepainter.cpp
@@ -371,27 +371,22 @@ void PagePainter::paintCroppedPageOnPainter( QPainter * destPainter, const Okula
                     recolor(&backImage, Okular::Settings::recolorForeground(), Okular::Settings::recolorBackground());
                     break;
                 case Okular::SettingsCore::EnumRenderMode::BlackWhite:
-                    // Manual Gray and Contrast
-                    unsigned int * data = reinterpret_cast<unsigned int *>(backImage.bits());
-                    int val, pixels = backImage.width() * backImage.height(),
-                        con = Okular::Settings::bWContrast(), thr = 255 - Okular::Settings::bWThreshold();
-                    for( int i = 0; i < pixels; ++i )
-                    {
-                        val = qGray( data[i] );
-                        if ( val > thr )
-                            val = 128 + (127 * (val - thr)) / (255 - thr);
-                        else if ( val < thr )
-                            val = (128 * val) / thr;
-                        if ( con > 2 )
-                        {
-                            val = con * ( val - thr ) / 2 + thr;
-                            if ( val > 255 )
-                                val = 255;
-                            else if ( val < 0 )
-                                val = 0;
-                        }
-                        data[i] = qRgba( val, val, val, 255 );
-                    }
+                    blackWhite(&backImage, Okular::Settings::bWContrast(), Okular::Settings::bWThreshold());
+                    break;
+                case Okular::SettingsCore::EnumRenderMode::InvertLightness:
+                    invertLightness(&backImage);
+                    break;
+                case Okular::SettingsCore::EnumRenderMode::InvertLuma:
+                    invertLuma(&backImage, 0.2126, 0.7152, 0.0722); // sRGB / Rec. 709 luma coefficients
+                    break;
+                case Okular::SettingsCore::EnumRenderMode::InvertLumaSymmetric:
+                    invertLuma(&backImage, 0.3333, 0.3334, 0.3333); // Symmetric coefficients, to keep colors saturated.
+                    break;
+                case Okular::SettingsCore::EnumRenderMode::HueShiftPositive:
+                    hueShiftPositive(&backImage);
+                    break;
+                case Okular::SettingsCore::EnumRenderMode::HueShiftNegative:
+                    hueShiftNegative(&backImage);
                     break;
             }
         }
@@ -839,6 +834,229 @@ void PagePainter::recolor(QImage *image, const QColor &foreground, const QColor
     }
 }
 
+void PagePainter::blackWhite(QImage *image, int contrast, int threshold)
+{
+    unsigned int * data = reinterpret_cast<unsigned int *>(image->bits());
+    int con = contrast;
+    int thr = 255 - threshold;
+
+    int pixels = image->width() * image->height();
+    for ( int i = 0; i < pixels; ++i )
+    {
+        // Piecewise linear function of val, through (0, 0), (thr, 128), (255, 255)
+        int val = qGray( data[i] );
+        if ( val > thr )
+            val = 128 + (127 * (val - thr)) / (255 - thr);
+        else if ( val < thr )
+            val = (128 * val) / thr;
+
+        // Linear contrast stretching through (thr, thr)
+        if ( con > 2 )
+        {
+            val = thr + ( val - thr ) * con / 2;
+            val = qBound( 0, val, 255 );
+        }
+        data[i] = qRgba( val, val, val, 255 );
+    }
+}
+
+void PagePainter::invertLightness(QImage* image)
+{
+    if (image->format() != QImage::Format_ARGB32_Premultiplied) {
+        qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
+        *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
+    }
+
+    Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
+
+    QRgb * data = reinterpret_cast<QRgb*>(image->bits());
+    int pixels = image->width() * image->height();
+    for ( int i = 0; i < pixels; ++i )
+    {
+        // Invert lightness of the pixel using the cylindric HSL color model.
+        // Algorithm is based on https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB (2019-03-17).
+        // Important simplifications are that inverting lightness does not change chroma and hue.
+        // This means the sector (of the chroma/hue plane) is not changed,
+        // so we can use a linear calculation after determining the sector using qMin() and qMax().
+        uchar R = qRed( data[ i ] );
+        uchar G = qGreen( data[ i ] );
+        uchar B = qBlue( data[ i ] );
+
+        // Get only the needed HSL components. These are chroma C and the common component m.
+        // Get common component m
+        uchar m = qMin( R, qMin( G, B ) );
+        // Remove m from color components
+        R -= m;
+        G -= m;
+        B -= m;
+        // Get chroma C
+        uchar C = qMax( R, qMax( G, B ) );
+
+        // Get common component m' after inverting lightness L.
+        // Hint: Lightness L = m + C / 2; L' = 255 - L = 255 - (m + C / 2) => m' = 255 - C - m
+        uchar m_ = 255 - C - m;
+
+        // Add m' to color compontents
+        R += m_;
+        G += m_;
+        B += m_;
+
+        // Save new color
+        data[i] = qRgba( R, G, B, 255 );
+    }
+}
+
+void PagePainter::invertLuma(QImage* image, float Y_R, float Y_G, float Y_B)
+{
+    if (image->format() != QImage::Format_ARGB32_Premultiplied) {
+        qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
+        *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
+    }
+
+    Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
+
+    QRgb * data = reinterpret_cast<QRgb*>(image->bits());
+    int pixels = image->width() * image->height();
+    for ( int i = 0; i < pixels; ++i )
+    {
+        uchar R = qRed( data[ i ] );
+        uchar G = qGreen( data[ i ] );
+        uchar B = qBlue( data[ i ] );
+
+        invertLumaPixel(R, G, B, Y_R, Y_G, Y_B);
+
+        // Save new color
+        data[i] = qRgba( R, G, B, 255 );
+    }
+}
+
+void PagePainter::invertLumaPixel(uchar &R, uchar &G, uchar &B, float Y_R, float Y_G, float Y_B) {
+    // Invert luma of the pixel using the bicone HCY color model, stretched to cylindric HSY.
+    // Algorithm is based on https://en.wikipedia.org/wiki/HSL_and_HSV#Luma,_chroma_and_hue_to_RGB (2019-03-19).
+    // For an illustration see https://experilous.com/1/product/make-it-colorful/ (2019-03-19).
+
+    // Special case: The algorithm does not work when hue is undefined.
+    if (R == G && G == B) {
+        R = 255 - R;
+        G = 255 - G;
+        B = 255 - B;
+        return;
+    }
+
+    // Get input and output luma Y, Y_inv in range 0..255
+    float Y = R * Y_R + G * Y_G + B * Y_B;
+    float Y_inv = 255 - Y;
+
+    // Get common component m and remove from color components.
+    // This moves us to the bottom faces of the HCY bicone, i. e. we get C and X in R, G, B.
+    uint_fast8_t m = qMin( R, qMin( G, B ) );
+    R -= m;
+    G -= m;
+    B -= m;
+
+    // We operate in a hue plane of the luma/chroma/hue bicone.
+    // The hue plane is a triangle.
+    // This bicone is distorted, so we can not simply mirror the triangle.
+    // We need to stretch it to a luma/saturation rectangle, so we need to stretch chroma C and the proportional X.
+
+    // First, we need to calculate luma Y_full_C for the outer corner of the triangle.
+    // Then we can interpolate the max chroma C_max, C_inv_max for our luma Y, Y_inv.
+    // Then we calculate C_inv and X_inv by scaling them by the ratio of C_max and C_inv_max.
+
+    // Calculate luma Y_full_C (in range equivalent to gray 0..255) for chroma = 1 at this hue.
+    // Piecewise linear, with the corners of the bicone at the sum of one or two luma coefficients.
+    float Y_full_C;
+    if (R >= B && B >= G) {
+        Y_full_C = 255 * Y_R + 255 * Y_B * B / R;
+    } else if (R >= G && G >= B) {
+        Y_full_C = 255 * Y_R + 255 * Y_G * G / R;
+    } else if (G >= R && R >= B) {
+        Y_full_C = 255 * Y_G + 255 * Y_R * R / G;
+    } else if (G >= B && B >= R) {
+        Y_full_C = 255 * Y_G + 255 * Y_B * B / G;
+    } else if (B >= G && G >= R) {
+        Y_full_C = 255 * Y_B + 255 * Y_G * G / B;
+    } else {
+        Y_full_C = 255 * Y_B + 255 * Y_R * R / B;
+    }
+
+    // Calculate C_max, C_inv_max, to scale C and X.
+    float C_max, C_inv_max;
+    if (Y >= Y_full_C) {
+        C_max = Y_inv / (255 - Y_full_C);
+    } else {
+        C_max = Y / Y_full_C;
+    }
+    if (Y_inv >= Y_full_C) {
+        C_inv_max = Y / (255 - Y_full_C);
+    } else {
+        C_inv_max = Y_inv / Y_full_C;
+    }
+
+    // Scale C and X. C and X already lie in R, G, B.
+    float C_scale = C_inv_max / C_max;
+    float R_ = R * C_scale;
+    float G_ = G * C_scale;
+    float B_ = B * C_scale;
+
+    // Calculate missing luma (in range 0..255), to get common component m_inv
+    float m_inv = Y_inv - (Y_R * R_ + Y_G * G_ + Y_B * B_);
+
+    // Add m_inv to color compontents
+    R_ += m_inv;
+    G_ += m_inv;
+    B_ += m_inv;
+
+    // Return colors rounded
+    R = R_ + 0.5;
+    G = G_ + 0.5;
+    B = B_ + 0.5;
+}
+
+void PagePainter::hueShiftPositive(QImage* image)
+{
+    if (image->format() != QImage::Format_ARGB32_Premultiplied) {
+        qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
+        *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
+    }
+
+    Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
+
+    QRgb * data = reinterpret_cast<QRgb*>(image->bits());
+    int pixels = image->width() * image->height();
+    for ( int i = 0; i < pixels; ++i )
+    {
+        uchar R = qRed( data[ i ] );
+        uchar G = qGreen( data[ i ] );
+        uchar B = qBlue( data[ i ] );
+
+        // Save new color
+        data[i] = qRgba( B, R, G, 255 );
+    }
+}
+
+void PagePainter::hueShiftNegative(QImage* image)
+{
+    if (image->format() != QImage::Format_ARGB32_Premultiplied) {
+        qCWarning(OkularUiDebug) << "Wrong image format! Converting...";
+        *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied);
+    }
+
+    Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied);
+
+    QRgb * data = reinterpret_cast<QRgb*>(image->bits());
+    int pixels = image->width() * image->height();
+    for ( int i = 0; i < pixels; ++i )
+    {
+        uchar R = qRed( data[ i ] );
+        uchar G = qGreen( data[ i ] );
+        uchar B = qBlue( data[ i ] );
+
+        // Save new color
+        data[i] = qRgba( G, B, R, 255 );
+    }
+}
+
 /** Private Helpers :: Image Drawing **/
 // from Arthur - qt4
 static inline int qt_div_255(int x) { return (x + (x>>8) + 0x80) >> 8; }
diff --git a/ui/pagepainter.h b/ui/pagepainter.h
index e35bd2f52..b09274aee 100644
--- a/ui/pagepainter.h
+++ b/ui/pagepainter.h
@@ -56,6 +56,27 @@ class Q_DECL_EXPORT PagePainter
     private:
         static void cropPixmapOnImage( QImage & dest, const QPixmap * src, const QRect r );
         static void recolor(QImage *image, const QColor &foreground, const QColor &background);
+        static void blackWhite(QImage *image, int contrast, int threshold);
+        static void invertLightness(QImage *image);
+        /**
+         * Inverts luma of @p image using the luma coefficients @p Y_R, @p Y_G, @p Y_B (should sum up to 1),
+         * and assuming linear 8bit RGB color space.
+         */
+        static void invertLuma(QImage *image, float Y_R, float Y_G, float Y_B);
+        /**
+         * Inverts luma of a pixel given in @p R, @p G, @p B,
+         * using the luma coefficients @p Y_R, @p Y_G, @p Y_B (should sum up to 1),
+         * and assuming linear 8bit RGB color space.
+         */
+        static void invertLumaPixel(uchar &R, uchar &G, uchar &B, float Y_R, float Y_G, float Y_B);
+        /**
+         * Shifts hue of each pixel by 120 degrees, by simply swapping channels.
+         */
+        static void hueShiftPositive(QImage *image);
+        /**
+         * Shifts hue of each pixel by 240 degrees, by simply swapping channels.
+         */
+        static void hueShiftNegative(QImage *image);
 
         // set the alpha component of the image to a given value
         static void changeImageAlpha( QImage & image, unsigned int alpha );


More information about the kde-doc-english mailing list