2018-12-27 08:39:10 -05:00
/*******************************************************************************
* util / imageutil . cpp *
* *
* Copyright ( c ) 2010 Retroshare Team < retroshare . project @ gmail . com > *
* *
* This program is free software : you can redistribute it and / or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation , either version 3 of the *
* License , or ( at your option ) any later version . *
* *
* This program is distributed in the hope that it will be useful , *
* but WITHOUT ANY WARRANTY ; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the *
* GNU Affero General Public License for more details . *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with this program . If not , see < https : //www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
2015-12-06 17:00:29 -05:00
# include "imageutil.h"
# include "util/misc.h"
2018-01-27 14:22:31 -05:00
# include "util/rstime.h"
2015-12-06 17:00:29 -05:00
2016-09-24 10:12:44 -04:00
# include <QApplication>
2015-12-06 17:00:29 -05:00
# include <QByteArray>
# include <QImage>
2016-09-24 10:12:44 -04:00
# include <QMessageBox>
# include <QString>
# include <QTextCursor>
2015-12-06 17:00:29 -05:00
# include <QTextDocumentFragment>
2017-06-08 17:00:33 -04:00
# include <QBuffer>
# include <QtGlobal>
2017-06-09 17:29:39 -04:00
# include <QSet>
2017-10-24 12:23:22 -04:00
# include <cmath>
2017-06-08 17:00:33 -04:00
# include <iostream>
2015-12-06 17:00:29 -05:00
ImageUtil : : ImageUtil ( ) { }
void ImageUtil : : extractImage ( QWidget * window , QTextCursor cursor )
{
cursor . movePosition ( QTextCursor : : Left , QTextCursor : : MoveAnchor , 1 ) ;
cursor . movePosition ( QTextCursor : : Right , QTextCursor : : KeepAnchor , 2 ) ;
QString imagestr = cursor . selection ( ) . toHtml ( ) ;
bool success = false ;
int start = imagestr . indexOf ( " base64, " ) + 7 ;
int stop = imagestr . indexOf ( " \" " , start ) ;
int length = stop - start ;
if ( ( start > = 0 ) & & ( length > 0 ) )
{
QByteArray ba = QByteArray : : fromBase64 ( imagestr . mid ( start , length ) . toLatin1 ( ) ) ;
QImage image = QImage : : fromData ( ba ) ;
if ( ! image . isNull ( ) )
{
QString file ;
success = true ;
if ( misc : : getSaveFileName ( window , RshareSettings : : LASTDIR_IMAGES , " Save Picture File " , " Pictures (*.png *.xpm *.jpg) " , file ) )
{
if ( ! image . save ( file , 0 , 100 ) )
if ( ! image . save ( file + " .png " , 0 , 100 ) )
QMessageBox : : warning ( window , QApplication : : translate ( " ImageUtil " , " Save image " ) , QApplication : : translate ( " ImageUtil " , " Cannot save the image, invalid filename " ) ) ;
}
}
}
if ( ! success )
{
QMessageBox : : warning ( window , QApplication : : translate ( " ImageUtil " , " Save image " ) , QApplication : : translate ( " ImageUtil " , " Not an image " ) ) ;
}
}
2015-12-12 16:41:15 -05:00
2017-10-23 11:04:20 -04:00
bool ImageUtil : : optimizeSize ( QString & html , const QImage & original , QImage & optimized , int maxPixels , int maxBytes )
2017-06-08 17:00:33 -04:00
{
2017-06-09 19:43:02 -04:00
//nothing to do if it fits into the limits
2017-06-08 17:00:33 -04:00
optimized = original ;
if ( ( maxPixels < = 0 ) | | ( optimized . width ( ) * optimized . height ( ) < = maxPixels ) ) {
2017-06-10 18:31:20 -04:00
if ( checkSize ( html , optimized , maxBytes ) < = maxBytes ) {
2017-10-23 11:04:20 -04:00
return true ;
2017-06-08 17:00:33 -04:00
}
}
2017-06-09 17:29:39 -04:00
QVector < QRgb > ct ;
quantization ( original , ct ) ;
2017-06-08 17:00:33 -04:00
//Downscale the image to fit into maxPixels
2017-06-10 18:31:20 -04:00
double whratio = ( qreal ) original . width ( ) / ( qreal ) original . height ( ) ;
int maxwidth ;
2017-11-05 12:58:07 -05:00
if ( maxPixels > 0 ) {
int maxwidth2 = ( int ) sqrt ( ( double ) ( maxPixels ) * whratio ) ;
maxwidth = ( original . width ( ) > maxwidth2 ) ? maxwidth2 : original . width ( ) ;
} else
2017-06-10 18:31:20 -04:00
maxwidth = original . width ( ) ;
2017-10-24 12:23:22 -04:00
int minwidth = ( int ) sqrt ( 100.0 * whratio ) ;
2017-06-10 18:31:20 -04:00
2017-10-23 11:04:20 -04:00
//if maxBytes not defined, do not reduce color space, just downscale
if ( maxBytes < = 0 ) {
checkSize ( html , optimized = original . scaledToWidth ( maxwidth , Qt : : SmoothTransformation ) , maxBytes ) ;
return true ;
}
2017-06-10 18:31:20 -04:00
//Use binary search to find a suitable image size + linear regression to guess the file size
2019-07-05 07:41:30 -04:00
double maxsize = ( double ) checkSize ( html , optimized = original . scaledToWidth ( maxwidth , Qt : : SmoothTransformation ) . convertToFormat ( QImage : : Format_Indexed8 , ct , Qt : : ThresholdDither ) , maxBytes ) ;
2017-10-23 11:04:20 -04:00
if ( maxsize < = maxBytes ) return true ; //success
2019-07-05 07:41:30 -04:00
double minsize = ( double ) checkSize ( html , optimized = original . scaledToWidth ( minwidth , Qt : : SmoothTransformation ) . convertToFormat ( QImage : : Format_Indexed8 , ct , Qt : : ThresholdDither ) , maxBytes ) ;
2017-10-23 11:04:20 -04:00
if ( minsize > maxBytes ) return false ; //impossible
2017-06-10 18:31:20 -04:00
// std::cout << "maxS: " << maxsize << " minS: " << minsize << std::endl;
// std::cout << "maxW: " << maxwidth << " minW: " << minwidth << std::endl;
int region = 500 ;
2017-06-09 19:43:02 -04:00
bool success = false ;
2019-07-04 13:07:02 -04:00
int latestgood = 0 ;
2017-06-08 17:00:33 -04:00
do {
2017-06-10 18:31:20 -04:00
double m = ( maxsize - minsize ) / ( ( double ) maxwidth * ( double ) maxwidth / whratio - ( double ) minwidth * ( double ) minwidth / whratio ) ;
double b = maxsize - m * ( ( double ) maxwidth * ( double ) maxwidth / whratio ) ;
double a = ( ( double ) ( maxBytes - region / 2 ) - b ) / m ; //maxBytes - region/2 target the center of the accepted region
2017-10-24 12:23:22 -04:00
int nextwidth = ( int ) sqrt ( a * whratio ) ;
2019-07-05 07:41:30 -04:00
int nextsize = checkSize ( html , optimized = original . scaledToWidth ( nextwidth , Qt : : SmoothTransformation ) . convertToFormat ( QImage : : Format_Indexed8 , ct , Qt : : ThresholdDither ) , maxBytes ) ;
2017-06-10 18:31:20 -04:00
if ( nextsize < = maxBytes ) {
minsize = nextsize ;
minwidth = nextwidth ;
2019-07-04 13:07:02 -04:00
if ( nextsize > = ( maxBytes - region ) | | //the file size is close enough to the limit
latestgood > = nextsize ) { //The algorithm does not converge anymore
2017-06-10 18:31:20 -04:00
success = true ;
2019-07-04 13:07:02 -04:00
} else {
latestgood = nextsize ;
}
2017-06-10 18:31:20 -04:00
} else {
maxsize = nextsize ;
maxwidth = nextwidth ;
}
// std::cout << "maxS: " << maxsize << " minS: " << minsize << std::endl;
// std::cout << "maxW: " << maxwidth << " minW: " << minwidth << std::endl;
} while ( ! success ) ;
2017-10-23 11:04:20 -04:00
return true ;
2017-06-10 18:31:20 -04:00
//html = html.arg(original.width());
//std::cout << html.toStdString() << std::endl;
2017-06-08 17:00:33 -04:00
}
2017-06-10 18:31:20 -04:00
int ImageUtil : : checkSize ( QString & embeddedImage , const QImage & img , int maxBytes )
2017-06-08 17:00:33 -04:00
{
2018-01-27 14:22:31 -05:00
rstime : : RsScopeTimer st ( " Check size " ) ;
2017-06-09 19:43:02 -04:00
2017-06-08 17:00:33 -04:00
QByteArray bytearray ;
QBuffer buffer ( & bytearray ) ;
2017-06-10 18:31:20 -04:00
int size = 0 ;
2017-06-08 17:00:33 -04:00
2017-10-23 11:04:20 -04:00
//std::cout << QString("Trying image: format PNG, size %1x%2, colors %3\n").arg(img.width()).arg(img.height()).arg(img.colorCount()).toStdString();
2017-06-08 17:00:33 -04:00
if ( buffer . open ( QIODevice : : WriteOnly ) ) {
if ( img . save ( & buffer , " PNG " , 0 ) ) {
2017-06-10 18:31:20 -04:00
size = bytearray . length ( ) * 4 / 3 ;
if ( ( maxBytes > 0 ) & & ( size > maxBytes ) ) // *4/3 for base64
2017-06-08 17:00:33 -04:00
{
2017-10-23 11:04:20 -04:00
//std::cout << QString("\tToo large, size: %1, limit: %2 bytes\n").arg(bytearray.length() * 4/3).arg(maxBytes).toStdString();
2017-06-08 17:00:33 -04:00
} else {
2017-10-23 11:04:20 -04:00
//std::cout << QString("\tOK, size: %1, limit: %2 bytes\n").arg(bytearray.length() * 4/3).arg(maxBytes).toStdString();
2017-06-09 19:43:02 -04:00
QByteArray encodedByteArray = bytearray . toBase64 ( ) ;
2017-06-10 18:31:20 -04:00
//embeddedImage = "<img width=\"%1\" src=\"data:image/png;base64,";
2017-06-09 19:43:02 -04:00
embeddedImage = " <img src= \" data:image/png;base64, " ;
embeddedImage . append ( encodedByteArray ) ;
embeddedImage . append ( " \" > " ) ;
2017-06-08 17:00:33 -04:00
}
} else {
std : : cerr < < " ImageUtil: image can't be saved to buffer " < < std : : endl ;
}
buffer . close ( ) ;
bytearray . clear ( ) ;
} else {
std : : cerr < < " ImageUtil: buffer can't be opened " < < std : : endl ;
}
2017-06-10 18:31:20 -04:00
return size ;
2017-06-08 17:00:33 -04:00
}
2017-06-09 17:29:39 -04:00
bool redLessThan ( const QRgb & c1 , const QRgb & c2 )
{
return qRed ( c1 ) < qRed ( c2 ) ;
}
bool greenLessThan ( const QRgb & c1 , const QRgb & c2 )
{
return qGreen ( c1 ) < qGreen ( c2 ) ;
}
bool blueLessThan ( const QRgb & c1 , const QRgb & c2 )
{
return qBlue ( c1 ) < qBlue ( c2 ) ;
}
//median cut algoritmh
void ImageUtil : : quantization ( const QImage & img , QVector < QRgb > & palette )
{
2019-07-05 07:41:30 -04:00
int bits = 8 ; // bits/pixel
2017-06-09 19:43:02 -04:00
int samplesize = 100000 ; //only take this many color samples
2018-01-27 14:22:31 -05:00
rstime : : RsScopeTimer st ( " Quantization " ) ;
2017-06-09 17:29:39 -04:00
QSet < QRgb > colors ;
//collect color information
2017-06-09 19:43:02 -04:00
int imgsize = img . width ( ) * img . height ( ) ;
int width = img . width ( ) ;
samplesize = qMin ( samplesize , imgsize ) ;
double sampledist = ( double ) imgsize / ( double ) samplesize ;
for ( double i = 0 ; i < imgsize ; i + = sampledist ) {
QRgb pixel = img . pixel ( ( int ) i % width , ( int ) i / width ) ;
colors . insert ( pixel ) ;
2017-06-09 17:29:39 -04:00
}
QList < QRgb > colorlist = colors . toList ( ) ;
//don't do the algoritmh if we have less than 16 different colors
if ( colorlist . size ( ) < = ( 1 < < bits ) ) {
for ( int i = 0 ; i < colors . count ( ) ; + + i )
palette . append ( colorlist [ i ] ) ;
} else {
quantization ( colorlist . begin ( ) , colorlist . end ( ) , bits , palette ) ;
}
}
void ImageUtil : : quantization ( QList < QRgb > : : iterator begin , QList < QRgb > : : iterator end , int depth , QVector < QRgb > & palette )
{
//the buckets are ready
if ( depth = = 0 ) {
avgbucket ( begin , end , palette ) ;
return ;
}
//nothing to do
int count = end - begin ;
if ( count = = 1 ) {
palette . append ( * begin ) ;
return ;
}
//widest color channel
int rl = 255 ;
int gl = 255 ;
int bl = 255 ;
int rh = 0 ;
int gh = 0 ;
int bh = 0 ;
for ( QList < QRgb > : : iterator it = begin ; it < end ; + + it ) {
rl = qMin ( rl , qRed ( * it ) ) ;
gl = qMin ( gl , qGreen ( * it ) ) ;
bl = qMin ( bl , qBlue ( * it ) ) ;
rh = qMax ( rh , qRed ( * it ) ) ;
gh = qMax ( gh , qGreen ( * it ) ) ;
bh = qMax ( bh , qBlue ( * it ) ) ;
}
int red = rh - rl ;
int green = gh - gl ;
int blue = bh - bl ;
//order by the widest channel
if ( red > green )
if ( red > blue )
qSort ( begin , end , redLessThan ) ;
else
qSort ( begin , end , blueLessThan ) ;
else
if ( green > blue )
qSort ( begin , end , greenLessThan ) ;
else
qSort ( begin , end , blueLessThan ) ;
//split into two buckets
QList < QRgb > : : iterator split = begin + count / 2 ;
quantization ( begin , split , depth - 1 , palette ) ;
quantization ( split , end , depth - 1 , palette ) ;
}
void ImageUtil : : avgbucket ( QList < QRgb > : : iterator begin , QList < QRgb > : : iterator end , QVector < QRgb > & palette )
{
int red = 0 ;
int green = 0 ;
int blue = 0 ;
int count = end - begin ;
for ( QList < QRgb > : : iterator it = begin ; it < end ; + + it ) {
red + = qRed ( * it ) ;
green + = qGreen ( * it ) ;
blue + = qBlue ( * it ) ;
}
QRgb color = qRgb ( red / count , green / count , blue / count ) ;
palette . append ( color ) ;
}