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 );
