[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