Commit aa683348 authored by David Hurka's avatar David Hurka 🐬 Committed by Albert Astals Cid
Browse files

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

parent 9694113a
Pipeline #17460 passed with stage
in 18 minutes and 58 seconds
......@@ -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();
}
......
......@@ -107,6 +107,31 @@
<string>Convert to Black &amp; 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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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; }
......
......@@ -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 );
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment