[app] Improved rendering of book covers

Especially narrow ones.
This commit is contained in:
Slava Monich 2021-11-01 00:40:49 +02:00
parent 491cfbd8b2
commit d4977b1a1f
4 changed files with 206 additions and 62 deletions

View file

@ -1,20 +1,21 @@
/* /*
Copyright (C) 2015 Jolla Ltd. Copyright (C) 2015-2021 Jolla Ltd.
Contact: Slava Monich <slava.monich@jolla.com> Copyright (C) 2015-2021 Slava Monich <slava.monich@jolla.com>
You may use this file under the terms of BSD license as follows: You may use this file under the terms of the BSD license as follows:
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions modification, are permitted provided that the following conditions
are met: are met:
* Redistributions of source code must retain the above copyright * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer. notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright * Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution. documentation and/or other materials provided with the distribution.
* Neither the name of the Jolla Ltd nor the * Neither the names of the copyright holders nor the names of its
names of its contributors may be used to endorse or promote products contributors may be used to endorse or promote products derived
derived from this software without specific prior written permission. from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
@ -60,7 +61,7 @@ CoverBackground {
width: grid.cellWidth width: grid.cellWidth
height: grid.cellHeight height: grid.cellHeight
borderWidth: 0 borderWidth: 0
stretch: true mode: BookCover.Stretch
} }
} }
@ -95,7 +96,7 @@ CoverBackground {
synchronous: true synchronous: true
book: root.book ? root.book : null book: root.book ? root.book : null
defaultCover: "images/default-cover.jpg" defaultCover: "images/default-cover.jpg"
stretch: true mode: BookCover.Fill
} }
BooksTitleText { BooksTitleText {

View file

@ -103,6 +103,7 @@ Item {
borderRadius: _borderRadius borderRadius: _borderRadius
borderWidth: book ? _borderWidth : 0 borderWidth: book ? _borderWidth : 0
borderColor: _borderColor borderColor: _borderColor
mode: BookCover.Bottom
opacity: (copyingIn || copyingOut) ? 0.1 : 1 opacity: (copyingIn || copyingOut) ? 0.1 : 1
Behavior on opacity { FadeAnimation { } } Behavior on opacity { FadeAnimation { } }

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2015-2018 Jolla Ltd. * Copyright (C) 2015-2021 Jolla Ltd.
* Copyright (C) 2015-2018 Slava Monich <slava.monich@jolla.com> * Copyright (C) 2015-2021 Slava Monich <slava.monich@jolla.com>
* *
* You may use this file under the terms of the BSD license as follows: * You may use this file under the terms of the BSD license as follows:
* *
@ -8,15 +8,15 @@
* modification, are permitted provided that the following conditions * modification, are permitted provided that the following conditions
* are met: * are met:
* *
* * Redistributions of source code must retain the above copyright * 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer. * notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright * 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in * notice, this list of conditions and the following disclaimer
* the documentation and/or other materials provided with the * in the documentation and/or other materials provided with the
* distribution. * distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors * 3. Neither the names of the copyright holders nor the names of its
* may be used to endorse or promote products derived from this * contributors may be used to endorse or promote products derived
* software without specific prior written permission. * from this software without specific prior written permission.
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
@ -50,19 +50,28 @@ public:
ScaleTask(QThreadPool* aPool, QImage aImage, int aWidth, int aHeight, ScaleTask(QThreadPool* aPool, QImage aImage, int aWidth, int aHeight,
bool aStretch); bool aStretch);
static QImage scale(QImage aImage, int aWidth, int aHeight, bool aStretch); static QImage scale(QImage aImage, int aWidth, int aHeight, bool aStretch);
static QColor leftBackground(const QImage& aImage);
static QColor rightBackground(const QImage& aImage);
static QColor topBackground(const QImage& aImage);
static QColor bottomBackground(const QImage& aImage);
static QColor pickColor(const QHash<QRgb,int>& aColorCounts);
void performTask(); void performTask();
public: public:
QImage iImage; QImage iImage;
QImage iScaledImage; QImage iScaledImage;
QColor iBackground1; // Left or top
QColor iBackground2; // Right or bottom
int iWidth; int iWidth;
int iHeight; int iHeight;
bool iStretch; bool iStretch;
}; };
BooksCoverWidget::ScaleTask::ScaleTask(QThreadPool* aPool, QImage aImage, BooksCoverWidget::ScaleTask::ScaleTask(QThreadPool* aPool, QImage aImage,
int aWidth, int aHeight, bool aStretch) : HarbourTask(aPool), int aWidth, int aHeight, bool aStretch) : HarbourTask(aPool), iImage(aImage),
iImage(aImage), iWidth(aWidth), iHeight(aHeight), iStretch(aStretch) iBackground1(Qt::transparent), iBackground2(Qt::transparent),
iWidth(aWidth), iHeight(aHeight),
iStretch(aStretch)
{ {
} }
@ -79,10 +88,106 @@ QImage BooksCoverWidget::ScaleTask::scale(QImage aImage,
} }
} }
// The idea is to pick the colors which occur more often
// at the edges of the picture.
QColor BooksCoverWidget::ScaleTask::leftBackground(const QImage& aImage)
{
QHash<QRgb,int> counts;
if (aImage.width() > 0) {
const int h = aImage.height();
for (int y = 0; y < h; y++) {
const QRgb left(aImage.pixelColor(0, y).rgb());
counts.insert(left, counts.value(left) + 1);
}
}
const QColor color(pickColor(counts));
HDEBUG(color << "left" << counts.count());
return color;
}
QColor BooksCoverWidget::ScaleTask::rightBackground(const QImage& aImage)
{
QHash<QRgb,int> counts;
const int w = aImage.width();
if (w > 0) {
const int h = aImage.height();
for (int y = 0; y < h; y++) {
const QRgb right(aImage.pixelColor(w - 1, y).rgb());
counts.insert(right, counts.value(right) + 1);
}
}
const QColor color(pickColor(counts));
HDEBUG(color << "right" << counts.count());
return color;
}
QColor BooksCoverWidget::ScaleTask::topBackground(const QImage& aImage)
{
QHash<QRgb,int> counts;
if (aImage.height() > 0) {
const int w = aImage.width();
for (int x = 0; x < w; x++) {
const QRgb left(aImage.pixelColor(x, 0).rgb());
counts.insert(left, counts.value(left) + 1);
}
}
const QColor color(pickColor(counts));
HDEBUG(color << "top" << counts.count());
return color;
}
QColor BooksCoverWidget::ScaleTask::bottomBackground(const QImage& aImage)
{
QHash<QRgb,int> counts;
const int h = aImage.height();
if (h > 0) {
const int w = aImage.width();
for (int x = 0; x < w; x++) {
const QRgb left(aImage.pixelColor(x, h - 1).rgb());
counts.insert(left, counts.value(left) + 1);
}
}
const QColor color(pickColor(counts));
HDEBUG(color << "bottom" << counts.count());
return color;
}
QColor BooksCoverWidget::ScaleTask::pickColor(const QHash<QRgb,int>& aCounts)
{
if (aCounts.size() > 0) {
QRgb rgb;
int max;
QHashIterator<QRgb,int> it(aCounts);
for (max = 0; it.hasNext();) {
it.next();
const int count = it.value();
if (max < count) {
max = count;
rgb = it.key();
}
}
return QColor(rgb);
}
return QColor();
}
void BooksCoverWidget::ScaleTask::performTask() void BooksCoverWidget::ScaleTask::performTask()
{ {
if (!iImage.isNull() && !isCanceled()) { if (!iImage.isNull() && !isCanceled()) {
iScaledImage = scale(iImage, iWidth, iHeight, iStretch); iScaledImage = scale(iImage, iWidth, iHeight, iStretch);
if (!isCanceled()) {
if (iScaledImage.width() < iWidth) {
iBackground1 = leftBackground(iScaledImage);
if (!isCanceled()) {
iBackground2 = rightBackground(iScaledImage);
}
} else if (iScaledImage.height() < iHeight) {
iBackground1 = topBackground(iScaledImage);
if (!isCanceled()) {
iBackground2 = bottomBackground(iScaledImage);
}
}
}
} }
} }
@ -204,7 +309,7 @@ BooksCoverWidget::BooksCoverWidget(QQuickItem* aParent) :
iBorderWidth(0), iBorderWidth(0),
iBorderRadius(0), iBorderRadius(0),
iBorderColor(Qt::transparent), iBorderColor(Qt::transparent),
iStretch(false), iMode(Bottom),
iSynchronous(false) iSynchronous(false)
{ {
setFlag(ItemHasContents, true); setFlag(ItemHasContents, true);
@ -264,13 +369,13 @@ void BooksCoverWidget::onCoverImageChanged()
scaleImage(wasEmpty); scaleImage(wasEmpty);
} }
void BooksCoverWidget::setStretch(bool aValue) void BooksCoverWidget::setMode(Mode aMode)
{ {
if (iStretch != aValue) { if (iMode != aMode) {
iStretch = aValue; iMode = aMode;
HDEBUG(aValue); HDEBUG(aMode);
scaleImage(); scaleImage();
Q_EMIT stretchChanged(); Q_EMIT modeChanged();
} }
} }
@ -362,12 +467,20 @@ void BooksCoverWidget::scaleImage(bool aWasEmpty)
} }
if (!iCoverImage.isNull()) { if (!iCoverImage.isNull()) {
const bool stretch = (iMode == Stretch);
if (iSynchronous) { if (iSynchronous) {
iScaledImage = ScaleTask::scale(iCoverImage, w, h, iStretch); iScaledImage = ScaleTask::scale(iCoverImage, w, h, stretch);
if (iScaledImage.width() < w) {
iBackground1 = ScaleTask::leftBackground(iScaledImage);
iBackground2 = ScaleTask::rightBackground(iScaledImage);
} else if (iScaledImage.height() < h) {
iBackground1 = ScaleTask::topBackground(iScaledImage);
iBackground2 = ScaleTask::bottomBackground(iScaledImage);
}
update(); update();
} else { } else {
(iScaleTask = new ScaleTask(iTaskQueue->pool(), iCoverImage, w, h, (iScaleTask = new ScaleTask(iTaskQueue->pool(), iCoverImage,
iStretch))->submit(this, SLOT(onScaleTaskDone())); w, h, stretch))->submit(this, SLOT(onScaleTaskDone()));
} }
} else { } else {
iScaledImage = QImage(); iScaledImage = QImage();
@ -389,6 +502,8 @@ void BooksCoverWidget::onScaleTaskDone()
const bool wasEmpty(empty()); const bool wasEmpty(empty());
HASSERT(iScaleTask == sender()); HASSERT(iScaleTask == sender());
iScaledImage = iScaleTask->iScaledImage; iScaledImage = iScaleTask->iScaledImage;
iBackground1 = iScaleTask->iBackground1;
iBackground2 = iScaleTask->iBackground2;
iScaleTask->release(this); iScaleTask->release(this);
iScaleTask = NULL; iScaleTask = NULL;
update(); update();
@ -403,28 +518,17 @@ void BooksCoverWidget::paint(QPainter* aPainter)
const qreal w = width(); const qreal w = width();
const qreal h = height(); const qreal h = height();
if (w > 0 && h > 0) { if (w > 0 && h > 0) {
qreal sw, sh;
// This has to be consistent with updateCenter() // This has to be consistent with updateCenter()
if (!iScaledImage.isNull()) { const qreal sh = (iScaledImage.height() && iMode == Bottom) ?
sw = iScaledImage.width(); qMax((qreal)iScaledImage.height(), w) : h;
sh = iScaledImage.height();
} else {
sw = w;
sh = h;
}
const int x = floor((w - sw)/2);
const int y = h - sh;
QPainterPath path; QPainterPath path;
qreal w1, h1, x1, y1; qreal w1, h1, x1, y1;
if (iBorderRadius > 0) { if (iBorderRadius > 0) {
// The border rectangle is no less that 3*radius // The border rectangle is no less that 3*radius
// and no more than the size of the item. // and no more than the size of the item.
const qreal d = 2*iBorderRadius; const qreal d = 2*iBorderRadius;
w1 = qMin(w, qMax(sw, 2*d)) - iBorderWidth; w1 = qMin(w, qMax(w, 2*d)) - iBorderWidth;
h1 = qMin(h, qMax(sh, 3*d)) - iBorderWidth; h1 = qMin(h, qMax(sh, 3*d)) - iBorderWidth;
x1 = floor((w - w1)/2); x1 = floor((w - w1)/2);
y1 = h - h1 - iBorderWidth/2; y1 = h - h1 - iBorderWidth/2;
@ -439,13 +543,41 @@ void BooksCoverWidget::paint(QPainter* aPainter)
path.closeSubpath(); path.closeSubpath();
aPainter->setClipPath(path); aPainter->setClipPath(path);
} else { } else {
w1 = sw - iBorderWidth; w1 = w - iBorderWidth;
h1 = sh - iBorderWidth; h1 = sh - iBorderWidth;
x1 = floor((w - w1)/2); x1 = floor((w - w1)/2);
y1 = h - h1 - iBorderWidth/2; y1 = h - h1 - iBorderWidth/2;
} }
const int x = floor((w - iScaledImage.width())/2);
const int y = floor(h - (sh + iScaledImage.height())/2);
if (!iScaledImage.isNull()) { if (!iScaledImage.isNull()) {
if (x > 0) {
aPainter->setPen(Qt::NoPen);
if (iBackground1.isValid()) {
aPainter->setBrush(QBrush(iBackground1));
aPainter->drawRect(0, y, x, iScaledImage.height());
}
if (iBackground2.isValid()) {
const int left = x + iScaledImage.width();
aPainter->setBrush(QBrush(iBackground2));
aPainter->drawRect(left, y, x, iScaledImage.height());
}
} else {
const int top = h - sh;
if (y > top) {
aPainter->setPen(Qt::NoPen);
if (iBackground1.isValid()) {
aPainter->setBrush(QBrush(iBackground1));
aPainter->drawRect(0, 0, w, y - top);
}
if (iBackground2.isValid()) {
aPainter->setBrush(QBrush(iBackground2));
aPainter->drawRect(0, y + iScaledImage.height(), w, y - top);
}
}
}
aPainter->drawImage(x, y, iScaledImage); aPainter->drawImage(x, y, iScaledImage);
} }
@ -475,7 +607,8 @@ void BooksCoverWidget::updateCenter()
if (iScaledImage.isNull()) { if (iScaledImage.isNull()) {
iCenter.setY(floor(h/2)); iCenter.setY(floor(h/2));
} else { } else {
iCenter.setY(floor(h - iScaledImage.height()/2)); const qreal sh = qMax((qreal)iScaledImage.height(), w);
iCenter.setY(floor(h - sh/2));
} }
if (iCenter != oldCenter) { if (iCenter != oldCenter) {

View file

@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2015-2018 Jolla Ltd. * Copyright (C) 2015-2021 Jolla Ltd.
* Copyright (C) 2015-2018 Slava Monich <slava.monich@jolla.com> * Copyright (C) 2015-2021 Slava Monich <slava.monich@jolla.com>
* *
* You may use this file under the terms of the BSD license as follows: * You may use this file under the terms of the BSD license as follows:
* *
@ -8,15 +8,15 @@
* modification, are permitted provided that the following conditions * modification, are permitted provided that the following conditions
* are met: * are met:
* *
* * Redistributions of source code must retain the above copyright * 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer. * notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright * 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in * notice, this list of conditions and the following disclaimer
* the documentation and/or other materials provided with the * in the documentation and/or other materials provided with the
* distribution. * distribution.
* * Neither the name of Nemo Mobile nor the names of its contributors * 3. Neither the names of the copyright holders nor the names of its
* may be used to endorse or promote products derived from this * contributors may be used to endorse or promote products derived
* software without specific prior written permission. * from this software without specific prior written permission.
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
@ -49,8 +49,8 @@ class BooksCoverWidget: public QQuickPaintedItem
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged) Q_PROPERTY(bool empty READ empty NOTIFY emptyChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(bool stretch READ stretch WRITE setStretch NOTIFY stretchChanged)
Q_PROPERTY(bool synchronous READ synchronous WRITE setSynchronous NOTIFY synchronousChanged) Q_PROPERTY(bool synchronous READ synchronous WRITE setSynchronous NOTIFY synchronousChanged)
Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
Q_PROPERTY(qreal borderWidth READ borderWidth WRITE setBorderWidth NOTIFY borderWidthChanged) Q_PROPERTY(qreal borderWidth READ borderWidth WRITE setBorderWidth NOTIFY borderWidthChanged)
Q_PROPERTY(qreal borderRadius READ borderRadius WRITE setBorderRadius NOTIFY borderRadiusChanged) Q_PROPERTY(qreal borderRadius READ borderRadius WRITE setBorderRadius NOTIFY borderRadiusChanged)
Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor NOTIFY borderColorChanged) Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor NOTIFY borderColorChanged)
@ -59,8 +59,15 @@ class BooksCoverWidget: public QQuickPaintedItem
Q_PROPERTY(qreal centerX READ centerX NOTIFY centerXChanged) Q_PROPERTY(qreal centerX READ centerX NOTIFY centerXChanged)
Q_PROPERTY(qreal centerY READ centerY NOTIFY centerYChanged) Q_PROPERTY(qreal centerY READ centerY NOTIFY centerYChanged)
Q_PROPERTY(QPoint center READ center NOTIFY centerChanged) Q_PROPERTY(QPoint center READ center NOTIFY centerChanged)
Q_ENUMS(Mode)
public: public:
enum Mode {
Fill,
Stretch,
Bottom
};
BooksCoverWidget(QQuickItem* aParent = NULL); BooksCoverWidget(QQuickItem* aParent = NULL);
~BooksCoverWidget(); ~BooksCoverWidget();
@ -82,8 +89,8 @@ public:
BooksBook* book() const { return iBook; } BooksBook* book() const { return iBook; }
void setBook(BooksBook* aBook); void setBook(BooksBook* aBook);
bool stretch() const { return iStretch; } Mode mode() const { return iMode; }
void setStretch(bool aValue); void setMode(Mode aMode);
bool synchronous() const { return iSynchronous; } bool synchronous() const { return iSynchronous; }
void setSynchronous(bool aValue); void setSynchronous(bool aValue);
@ -96,8 +103,8 @@ Q_SIGNALS:
void bookChanged(); void bookChanged();
void emptyChanged(); void emptyChanged();
void loadingChanged(); void loadingChanged();
void stretchChanged();
void synchronousChanged(); void synchronousChanged();
void modeChanged();
void borderWidthChanged(); void borderWidthChanged();
void borderRadiusChanged(); void borderRadiusChanged();
void borderColorChanged(); void borderColorChanged();
@ -124,6 +131,8 @@ private:
ScaleTask* iScaleTask; ScaleTask* iScaleTask;
QImage iScaledImage; QImage iScaledImage;
QImage iCoverImage; QImage iCoverImage;
QColor iBackground1;
QColor iBackground2;
BooksBook* iBook; BooksBook* iBook;
QImage* iDefaultImage; QImage* iDefaultImage;
qreal iBorderWidth; qreal iBorderWidth;
@ -132,7 +141,7 @@ private:
QUrl iDefaultCover; QUrl iDefaultCover;
QString iTitle; QString iTitle;
QPoint iCenter; QPoint iCenter;
bool iStretch; Mode iMode;
bool iSynchronous; bool iSynchronous;
}; };