2010-05-11 20:17:10 -04:00
/****************************************************************
* This file is distributed under the following license :
*
* Copyright ( c ) 2010 , Thomas Kister
*
* This program is free software ; you can redistribute it and / or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation ; either version 2
* 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 General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; if not , write to the Free Software
* Foundation , Inc . , 51 Franklin Street , Fifth Floor ,
* Boston , MA 02110 - 1301 , USA .
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
2012-09-18 18:59:23 -04:00
# include <QApplication>
2011-03-06 07:58:18 -05:00
# include <QTextBrowser>
2012-05-11 20:40:53 -04:00
# include <QtXml>
2012-09-18 18:59:23 -04:00
# include <QBuffer>
# include <QMessageBox>
# include <qmath.h>
2012-05-11 20:40:53 -04:00
2010-05-11 20:17:10 -04:00
# include "HandleRichText.h"
2011-05-01 18:26:41 -04:00
# include "gui/RetroShareLink.h"
2012-05-11 20:40:53 -04:00
# include "util/ObjectPainter.h"
2010-05-11 20:17:10 -04:00
2011-09-29 05:20:09 -04:00
# include <iostream>
2012-05-11 20:40:53 -04:00
/**
* The type of embedding we ' d like to do
*/
enum EmbeddedType
{
Ahref , ///< into <a></a>
Img ///< into <img/>
} ;
/**
* Base class for storing information about a given kind of embedding .
*
* Its only constructor is protected so it is impossible to instantiate it , and
* at the same time derived classes have to provide a type .
*/
class EmbedInHtml
{
protected :
EmbedInHtml ( EmbeddedType newType ) : myType ( newType ) { }
public :
const EmbeddedType myType ;
QRegExp myRE ;
} ;
2010-05-11 20:17:10 -04:00
2012-05-11 20:40:53 -04:00
/**
* This class is used to store information for embedding links into < a > < / a > tags .
*/
class EmbedInHtmlAhref : public EmbedInHtml
{
public :
EmbedInHtmlAhref ( ) : EmbedInHtml ( Ahref )
{
myRE . setPattern ( " ( \\ bretroshare://[^ \\ s]*)|( \\ bhttps?://[^ \\ s]*)|( \\ bfile://[^ \\ s]*)|( \\ bwww \\ .[^ \\ s]*) " ) ;
}
} ;
/**
* This class is used to store information for embedding smileys into < img / > tags .
*
* By default the QRegExp the variables are empty , which means it must be
* filled at runtime , typically when the smileys set is loaded . It can be
* either done by hand or by using one of the helper methods available .
*
* Note : The QHash uses only * one * smiley per key ( unlike soon - to - be - upgraded
* code out there ) .
*/
class EmbedInHtmlImg : public EmbedInHtml
{
public :
EmbedInHtmlImg ( ) : EmbedInHtml ( Img ) { }
QHash < QString , QString > smileys ;
} ;
2010-05-11 20:17:10 -04:00
2012-05-11 20:40:53 -04:00
/* global instance for embedding emoticons */
static EmbedInHtmlImg defEmbedImg ;
RsHtml : : RsHtml ( )
{
}
void RsHtml : : initEmoticons ( const QHash < QString , QString > & hash )
2010-05-11 20:17:10 -04:00
{
QString newRE ;
for ( QHash < QString , QString > : : const_iterator it = hash . begin ( ) ; it ! = hash . end ( ) ; + + it )
foreach ( QString smile , it . key ( ) . split ( " | " ) ) {
2010-09-15 10:32:09 -04:00
if ( smile . isEmpty ( ) ) {
continue ;
}
2012-05-11 20:40:53 -04:00
defEmbedImg . smileys . insert ( smile , it . value ( ) ) ;
2010-05-11 20:17:10 -04:00
newRE + = " ( " + QRegExp : : escape ( smile ) + " )| " ;
}
newRE . chop ( 1 ) ; // remove last |
2012-05-11 20:40:53 -04:00
defEmbedImg . myRE . setPattern ( newRE ) ;
}
bool RsHtml : : canReplaceAnchor ( QDomDocument & /*doc*/ , QDomElement & /*element*/ , const RetroShareLink & link )
{
switch ( link . type ( ) ) {
case RetroShareLink : : TYPE_UNKNOWN :
case RetroShareLink : : TYPE_FILE :
case RetroShareLink : : TYPE_PERSON :
case RetroShareLink : : TYPE_FORUM :
case RetroShareLink : : TYPE_CHANNEL :
case RetroShareLink : : TYPE_SEARCH :
case RetroShareLink : : TYPE_MESSAGE :
2013-06-10 09:27:24 -04:00
case RetroShareLink : : TYPE_EXTRAFILE :
case RetroShareLink : : TYPE_PRIVATE_CHAT :
case RetroShareLink : : TYPE_PUBLIC_MSG :
2012-05-11 20:40:53 -04:00
// not yet implemented
break ;
case RetroShareLink : : TYPE_CERTIFICATE :
return true ;
}
return false ;
}
void RsHtml : : anchorTextForImg ( QDomDocument & /*doc*/ , QDomElement & /*element*/ , const RetroShareLink & link , QString & text )
{
text = link . niceName ( ) ;
}
void RsHtml : : anchorStylesheetForImg ( QDomDocument & /*doc*/ , QDomElement & /*element*/ , const RetroShareLink & link , QString & styleSheet )
{
switch ( link . type ( ) ) {
case RetroShareLink : : TYPE_UNKNOWN :
case RetroShareLink : : TYPE_FILE :
case RetroShareLink : : TYPE_PERSON :
case RetroShareLink : : TYPE_FORUM :
case RetroShareLink : : TYPE_CHANNEL :
case RetroShareLink : : TYPE_SEARCH :
case RetroShareLink : : TYPE_MESSAGE :
2013-06-10 09:27:24 -04:00
case RetroShareLink : : TYPE_EXTRAFILE :
case RetroShareLink : : TYPE_PRIVATE_CHAT :
case RetroShareLink : : TYPE_PUBLIC_MSG :
2012-05-11 20:40:53 -04:00
// not yet implemented
break ;
case RetroShareLink : : TYPE_CERTIFICATE :
2012-05-12 19:44:48 -04:00
styleSheet = " QPushButton{ border-image: url(:/images/btn_blue.png) ;border-width: 4;padding: 0px 6px;font-size: 12px;color: white;} QPushButton:hover{ border-image: url(:/images/btn_blue_hover.png) ;} " ;
2012-05-11 20:40:53 -04:00
break ;
}
}
void RsHtml : : replaceAnchorWithImg ( QDomDocument & doc , QDomElement & element , QTextDocument * textDocument , const RetroShareLink & link )
{
if ( ! textDocument ) {
return ;
}
if ( ! link . valid ( ) ) {
return ;
}
if ( element . childNodes ( ) . length ( ) ! = 1 ) {
return ;
}
if ( ! canReplaceAnchor ( doc , element , link ) ) {
return ;
}
QString imgText ;
anchorTextForImg ( doc , element , link , imgText ) ;
QString styleSheet ;
anchorStylesheetForImg ( doc , element , link , styleSheet ) ;
QDomNode childNode = element . firstChild ( ) ;
/* build resource name */
QString resourceName = QString ( " %1_%2.png " ) . arg ( link . type ( ) ) . arg ( imgText ) ;
if ( ! textDocument - > resource ( QTextDocument : : ImageResource , QUrl ( resourceName ) ) . isValid ( ) ) {
/* draw a button on a pixmap */
QPixmap pixmap ;
ObjectPainter : : drawButton ( imgText , styleSheet , pixmap ) ;
/* add the image to the resource cache of the text document */
textDocument - > addResource ( QTextDocument : : ImageResource , QUrl ( resourceName ) , QVariant ( pixmap ) ) ;
}
element . removeChild ( childNode ) ;
/* replace text of the anchor with <img> */
QDomElement img = doc . createElement ( " img " ) ;
img . setAttribute ( " src " , resourceName ) ;
element . appendChild ( img ) ;
2010-05-11 20:17:10 -04:00
}
/**
* Parses a DOM tree and replaces text by HTML tags .
* The tree is traversed depth - first , but only through children of Element type
* nodes . Any other kind of node is terminal .
*
* If the node is of type Text , its data is checked against the user - provided
* regular expression . If there is a match , the text is cut in three parts : the
* preceding part that will be inserted before , the part to be replaced , and the
* following part which will be itself checked against the regular expression .
*
* The part to be replaced is sent to a user - provided functor that will create
* the necessary embedding and return a new Element node to be inserted .
*
* @ param [ in ] doc The whole DOM tree , necessary to create new nodes
* @ param [ in , out ] currentElement The current node ( which is of type Element )
* @ param [ in ] embedInfos The regular expression and the type of embedding to use
*/
2012-05-11 20:40:53 -04:00
void RsHtml : : embedHtml ( QTextDocument * textDocument , QDomDocument & doc , QDomElement & currentElement , EmbedInHtml & embedInfos , ulong flag )
2010-05-11 20:17:10 -04:00
{
if ( embedInfos . myRE . pattern ( ) . length ( ) = = 0 ) // we'll get stuck with an empty regexp
return ;
QDomNodeList children = currentElement . childNodes ( ) ;
for ( uint index = 0 ; index < children . length ( ) ; index + + ) {
2011-05-01 18:26:41 -04:00
QDomNode node = children . item ( index ) ;
if ( node . isElement ( ) ) {
2010-05-11 20:17:10 -04:00
// child is an element, we skip it if it's an <a> tag
2011-05-01 18:26:41 -04:00
QDomElement element = node . toElement ( ) ;
if ( element . tagName ( ) . toLower ( ) = = " head " ) {
// skip it
} else if ( element . tagName ( ) . toLower ( ) = = " a " ) {
2012-05-11 20:40:53 -04:00
// skip it
if ( embedInfos . myType = = Ahref ) {
// but add title if not available
if ( element . attribute ( " title " ) . isEmpty ( ) ) {
RetroShareLink link ( element . attribute ( " href " ) ) ;
if ( link . valid ( ) ) {
QString title = link . title ( ) ;
if ( ! title . isEmpty ( ) ) {
element . setAttribute ( " title " , title ) ;
}
if ( textDocument & & ( flag & RSHTML_FORMATTEXT_REPLACE_LINKS ) ) {
replaceAnchorWithImg ( doc , element , textDocument , link ) ;
}
}
} else {
if ( textDocument & & ( flag & RSHTML_FORMATTEXT_REPLACE_LINKS ) ) {
RetroShareLink link ( element . attribute ( " href " ) ) ;
if ( link . valid ( ) ) {
replaceAnchorWithImg ( doc , element , textDocument , link ) ;
}
}
2011-05-01 18:26:41 -04:00
}
}
} else {
2012-05-11 20:40:53 -04:00
embedHtml ( textDocument , doc , element , embedInfos , flag ) ;
2011-05-01 18:26:41 -04:00
}
2010-05-11 20:17:10 -04:00
}
2011-05-01 18:26:41 -04:00
else if ( node . isText ( ) ) {
2010-05-11 20:17:10 -04:00
// child is a text, we parse it
2011-05-01 18:26:41 -04:00
QString tempText = node . toText ( ) . data ( ) ;
2010-05-11 20:17:10 -04:00
if ( embedInfos . myRE . indexIn ( tempText ) = = - 1 )
continue ;
// there is at least one link inside, we start replacing
int currentPos = 0 ;
int nextPos = 0 ;
while ( ( nextPos = embedInfos . myRE . indexIn ( tempText , currentPos ) ) ! = - 1 ) {
// if nextPos == 0 it means the text begins by a link
if ( nextPos > 0 ) {
QDomText textPart = doc . createTextNode ( tempText . mid ( currentPos , nextPos - currentPos ) ) ;
2011-05-01 18:26:41 -04:00
currentElement . insertBefore ( textPart , node ) ;
2010-05-11 20:17:10 -04:00
index + + ;
}
// inserted tag
QDomElement insertedTag ;
switch ( embedInfos . myType ) {
2011-05-01 18:26:41 -04:00
case Ahref :
{
2012-05-12 12:04:18 -04:00
insertedTag = doc . createElement ( " a " ) ;
insertedTag . setAttribute ( " href " , embedInfos . myRE . cap ( 0 ) ) ;
insertedTag . appendChild ( doc . createTextNode ( embedInfos . myRE . cap ( 0 ) ) ) ;
2011-05-01 18:26:41 -04:00
RetroShareLink link ( embedInfos . myRE . cap ( 0 ) ) ;
2012-05-11 20:40:53 -04:00
if ( link . valid ( ) ) {
QString title = link . title ( ) ;
if ( ! title . isEmpty ( ) ) {
insertedTag . setAttribute ( " title " , title ) ;
}
if ( textDocument & & ( flag & RSHTML_FORMATTEXT_REPLACE_LINKS ) ) {
replaceAnchorWithImg ( doc , insertedTag , textDocument , link ) ;
}
}
2011-05-01 18:26:41 -04:00
}
2010-05-11 20:17:10 -04:00
break ;
2011-05-01 18:26:41 -04:00
case Img :
2010-05-11 20:17:10 -04:00
{
2011-05-01 18:26:41 -04:00
insertedTag = doc . createElement ( " img " ) ;
2010-05-11 20:17:10 -04:00
const EmbedInHtmlImg & embedImg = static_cast < const EmbedInHtmlImg & > ( embedInfos ) ;
insertedTag . setAttribute ( " src " , embedImg . smileys [ embedInfos . myRE . cap ( 0 ) ] ) ;
}
break ;
}
2012-05-11 20:40:53 -04:00
2012-05-12 12:04:18 -04:00
currentElement . insertBefore ( insertedTag , node ) ;
index + + ;
2010-05-11 20:17:10 -04:00
currentPos = nextPos + embedInfos . myRE . matchedLength ( ) ;
}
// text after the last link, only if there's one, don't touch the index
2011-05-01 18:26:41 -04:00
// otherwise decrement the index because we're going to remove node
2010-05-11 20:17:10 -04:00
if ( currentPos < tempText . length ( ) ) {
QDomText textPart = doc . createTextNode ( tempText . mid ( currentPos ) ) ;
2011-05-01 18:26:41 -04:00
currentElement . insertBefore ( textPart , node ) ;
2010-05-11 20:17:10 -04:00
}
else
index - - ;
2011-05-01 18:26:41 -04:00
currentElement . removeChild ( node ) ;
2010-05-11 20:17:10 -04:00
}
}
}
2014-01-18 17:55:14 -05:00
/**
* Save space and tab out of bracket that XML loose .
*
* @ param [ in ] text The text to save space .
* @ return Text with space saved .
*/
static QString saveSpace ( const QString text )
{
QString savedSpaceText = text ;
bool outBrackets = false , echapChar = false ;
QString keyName = " " ;
bool getKeyName = false ;
2014-08-07 19:07:16 -04:00
bool firstOutBracket = false ;
2014-01-18 17:55:14 -05:00
for ( int i = 0 ; i < savedSpaceText . length ( ) ; i + + ) {
QChar cursChar = savedSpaceText . at ( i ) ;
if ( getKeyName | | ( ! outBrackets & & keyName . isEmpty ( ) ) ) {
if ( ( cursChar = = QLatin1Char ( ' ' ) ) | | ( cursChar = = QLatin1Char ( ' > ' ) ) ) {
getKeyName = keyName . isEmpty ( ) ;
} else {
keyName . append ( cursChar . toLower ( ) ) ;
}
}
2014-08-07 19:07:16 -04:00
2014-01-18 17:55:14 -05:00
if ( cursChar = = QLatin1Char ( ' > ' ) ) {
2014-08-07 19:07:16 -04:00
if ( ! echapChar & & i > 0 ) { outBrackets = true ; firstOutBracket = true ; }
} else if ( cursChar = = QLatin1Char ( ' \t ' ) ) {
if ( outBrackets & & firstOutBracket & & ( keyName ! = " style " ) ) savedSpaceText . replace ( i , 1 , " " ) ;
2014-01-18 17:55:14 -05:00
} else if ( cursChar = = QLatin1Char ( ' ' ) ) {
2014-08-07 19:07:16 -04:00
if ( outBrackets & & firstOutBracket & & ( keyName ! = " style " ) ) savedSpaceText . replace ( i , 1 , " " ) ;
2014-01-18 17:55:14 -05:00
} else if ( cursChar = = QChar ( 0xA0 ) ) {
2014-08-07 19:07:16 -04:00
if ( outBrackets & & firstOutBracket & & ( keyName ! = " style " ) ) savedSpaceText . replace ( i , 1 , " " ) ;
2014-01-18 17:55:14 -05:00
} else if ( cursChar = = QLatin1Char ( ' < ' ) ) {
if ( ! echapChar ) { outBrackets = false ; getKeyName = true ; keyName . clear ( ) ; }
2014-08-07 19:07:16 -04:00
} else firstOutBracket = false ;
2014-01-18 17:55:14 -05:00
echapChar = ( cursChar = = QLatin1Char ( ' \\ ' ) ) ;
2014-08-07 19:07:16 -04:00
2014-01-18 17:55:14 -05:00
}
return savedSpaceText ;
}
2012-09-21 17:23:14 -04:00
QString RsHtml : : formatText ( QTextDocument * textDocument , const QString & text , ulong flag , const QColor & backgroundColor , qreal desiredContrast )
2011-05-15 16:21:14 -04:00
{
2011-06-07 18:28:07 -04:00
if ( flag = = 0 | | text . isEmpty ( ) ) {
2011-05-15 16:21:14 -04:00
// nothing to do
return text ;
}
2014-01-18 17:55:14 -05:00
QString formattedText = text ;
//remove all prepend char that make doc.setContent() fail
formattedText . remove ( 0 , text . indexOf ( " < " ) ) ;
// Save Space and Tab because doc loose it.
formattedText = saveSpace ( formattedText ) ;
QString errorMsg ; int errorLine ; int errorColumn ;
QDomDocument doc ;
if ( doc . setContent ( formattedText , & errorMsg , & errorLine , & errorColumn ) = = false ) {
2011-05-15 16:21:14 -04:00
// convert text with QTextBrowser
QTextBrowser textBrowser ;
textBrowser . setText ( text ) ;
2014-01-18 17:55:14 -05:00
formattedText = textBrowser . toHtml ( ) ;
formattedText . remove ( 0 , formattedText . indexOf ( " < " ) ) ;
formattedText = saveSpace ( formattedText ) ;
doc . setContent ( formattedText , & errorMsg , & errorLine , & errorColumn ) ;
2011-05-15 16:21:14 -04:00
}
QDomElement body = doc . documentElement ( ) ;
if ( flag & RSHTML_FORMATTEXT_EMBED_SMILEYS ) {
2012-05-11 20:40:53 -04:00
embedHtml ( textDocument , doc , body , defEmbedImg , flag ) ;
2011-05-15 16:21:14 -04:00
}
if ( flag & RSHTML_FORMATTEXT_EMBED_LINKS ) {
EmbedInHtmlAhref defEmbedAhref ;
2012-05-11 20:40:53 -04:00
embedHtml ( textDocument , doc , body , defEmbedAhref , flag ) ;
2011-05-15 16:21:14 -04:00
}
2014-01-18 17:55:14 -05:00
formattedText = doc . toString ( - 1 ) ; // -1 removes any annoying carriage return misinterpreted by QTextEdit
2012-03-31 11:20:19 -04:00
2012-09-21 17:23:14 -04:00
if ( flag & RSHTML_OPTIMIZEHTML_MASK ) {
optimizeHtml ( formattedText , flag & RSHTML_OPTIMIZEHTML_MASK , backgroundColor , desiredContrast ) ;
2012-04-24 11:34:22 -04:00
}
2012-01-30 19:06:24 -05:00
return formattedText ;
2011-05-15 16:21:14 -04:00
}
static void findElements ( QDomDocument & doc , QDomElement & currentElement , const QString & nodeName , const QString & nodeAttribute , QStringList & elements )
{
if ( nodeName . isEmpty ( ) ) {
return ;
}
QDomNodeList children = currentElement . childNodes ( ) ;
for ( uint index = 0 ; index < children . length ( ) ; index + + ) {
QDomNode node = children . item ( index ) ;
if ( node . isElement ( ) ) {
QDomElement element = node . toElement ( ) ;
if ( QString : : compare ( element . tagName ( ) , nodeName , Qt : : CaseInsensitive ) = = 0 ) {
if ( nodeAttribute . isEmpty ( ) ) {
// use text
elements . append ( element . text ( ) ) ;
} else {
QString attribute = element . attribute ( nodeAttribute ) ;
if ( attribute . isEmpty ( ) = = false ) {
elements . append ( attribute ) ;
}
}
continue ;
}
findElements ( doc , element , nodeName , nodeAttribute , elements ) ;
}
}
}
2012-05-11 20:40:53 -04:00
bool RsHtml : : findAnchors ( const QString & text , QStringList & urls )
2011-05-15 16:21:14 -04:00
{
2014-01-18 17:55:14 -05:00
QString errorMsg ; int errorLine ; int errorColumn ;
2011-05-15 16:21:14 -04:00
QDomDocument doc ;
2014-01-18 17:55:14 -05:00
if ( doc . setContent ( text , & errorMsg , & errorLine , & errorColumn ) = = false ) {
2012-05-11 20:40:53 -04:00
// convert text with QTextBrowser
QTextBrowser textBrowser ;
textBrowser . setText ( text ) ;
2014-01-18 17:55:14 -05:00
doc . setContent ( textBrowser . toHtml ( ) , & errorMsg , & errorLine , & errorColumn ) ;
2011-05-15 16:21:14 -04:00
}
QDomElement body = doc . documentElement ( ) ;
findElements ( doc , body , " a " , " href " , urls ) ;
return true ;
}
2012-01-30 19:06:24 -05:00
static void removeElement ( QDomElement & parentElement , QDomElement & element )
{
QDomNodeList children = element . childNodes ( ) ;
while ( children . length ( ) > 0 ) {
QDomNode childElement = element . removeChild ( children . item ( children . length ( ) - 1 ) ) ;
parentElement . insertAfter ( childElement , element ) ;
}
parentElement . removeChild ( element ) ;
}
2012-09-21 17:23:14 -04:00
static qreal linearizeColorComponent ( qreal v )
{
if ( v < = 0.03928 ) return v / 12.92 ;
else return pow ( ( v + 0.055 ) / 1.055 , 2.4 ) ;
}
static qreal getRelativeLuminance ( const QColor & c )
{
qreal r = linearizeColorComponent ( c . redF ( ) ) * 0.2126 ;
qreal g = linearizeColorComponent ( c . greenF ( ) ) * 0.7152 ;
qreal b = linearizeColorComponent ( c . blueF ( ) ) * 0.0722 ;
return r + g + b ;
}
/**
* @ brief Compute the contrast between two relative luminances .
* See : http : //www.w3.org/TR/2012/NOTE-WCAG20-TECHS-20120103/G18
* @ param lum1 , lum2 Relative luminances returned by getRelativeLuminance ( ) .
* @ return Contrast between 1 and 21.
*/
static qreal getContrastRatio ( qreal lum1 , qreal lum2 )
{
if ( lum2 > lum1 ) {
qreal t = lum1 ;
lum1 = lum2 ;
lum2 = t ;
}
return ( lum1 + 0.05 ) / ( lum2 + 0.05 ) ;
}
/**
* @ brief Find a color with the same hue that provides the desired contrast with bglum .
* @ param [ in , out ] val Name of the original color . Will be modified .
* @ param bglum Background ' s relative luminance as returned by getRelativeLuminance ( ) .
*/
static void findBestColor ( QString & val , qreal bglum , qreal desiredContrast )
{
2012-09-24 07:32:11 -04:00
# if QT_VERSION < 0x040600
// missing methods on class QColor
Q_UNUSED ( val ) ;
Q_UNUSED ( bglum ) ;
Q_UNUSED ( desiredContrast ) ;
# else
2012-09-21 17:23:14 -04:00
// Change text color to get a good contrast with the background
QColor c ( val ) ;
qreal lum = : : getRelativeLuminance ( c ) ;
// Keep text color darker/brighter than the bg if possible
qreal lowContrast = : : getContrastRatio ( bglum , 0.0 ) ;
qreal highContrast = : : getContrastRatio ( bglum , 1.0 ) ;
bool searchDown = ( lum < = bglum & & lowContrast > = desiredContrast )
| | ( lum > bglum & & highContrast < desiredContrast & & lowContrast > = highContrast ) ;
// There's no such thing as too much contrast on a bright background,
// but on a dark background it creates haloing which makes text hard to read.
// So we enforce desired contrast when the bg is dark.
if ( ! searchDown | | : : getContrastRatio ( lum , bglum ) < desiredContrast ) {
// Bisection search of the correct "lightness" to get the desired contrast
qreal minl = searchDown ? 0.0 : bglum ;
qreal maxl = searchDown ? bglum : 1.0 ;
do {
QColor d = c ;
qreal midl = ( minl + maxl ) / 2.0 ;
d . setHslF ( c . hslHueF ( ) , c . hslSaturationF ( ) , midl ) ;
qreal lum = : : getRelativeLuminance ( d ) ;
if ( ( : : getContrastRatio ( lum , bglum ) < desiredContrast ) ^ searchDown ) {
minl = midl ;
}
else {
maxl = midl ;
}
} while ( maxl - minl > 0.01 ) ;
c . setHslF ( c . hslHueF ( ) , c . hslSaturationF ( ) , minl ) ;
val = c . name ( ) ;
}
2012-09-24 07:32:11 -04:00
# endif // QT_VERSION < 0x040600
2012-09-21 17:23:14 -04:00
}
static void optimizeHtml ( QDomDocument & doc , QDomElement & currentElement , unsigned int flag , qreal bglum , qreal desiredContrast )
2011-09-29 05:20:09 -04:00
{
2012-01-30 19:06:24 -05:00
if ( currentElement . tagName ( ) . toLower ( ) = = " html " ) {
// change <html> to <span>
currentElement . setTagName ( " span " ) ;
}
QDomNode styleNode ;
bool addBR = false ;
2011-09-29 05:20:09 -04:00
QDomNodeList children = currentElement . childNodes ( ) ;
for ( uint index = 0 ; index < children . length ( ) ; ) {
QDomNode node = children . item ( index ) ;
2012-01-30 19:06:24 -05:00
// compress style attribute
styleNode = node . attributes ( ) . namedItem ( " style " ) ;
if ( styleNode . isAttr ( ) ) {
2012-03-31 11:20:19 -04:00
QDomAttr styleAttr = styleNode . toAttr ( ) ;
QString style = styleAttr . value ( ) . simplified ( ) ;
style . replace ( " margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; " , " margin:0px 0px 0px 0px; " ) ;
style . replace ( " ; " , " ; " ) ;
2012-09-21 17:23:14 -04:00
if ( flag ) {
2012-03-31 11:20:19 -04:00
QStringList styles = style . split ( ' ; ' ) ;
style . clear ( ) ;
foreach ( QString pair , styles ) {
if ( ! pair . trimmed ( ) . isEmpty ( ) ) {
QStringList keyvalue = pair . split ( ' : ' ) ;
if ( keyvalue . length ( ) = = 2 ) {
QString key = keyvalue . at ( 0 ) . trimmed ( ) ;
2012-09-21 17:23:14 -04:00
QString val = keyvalue . at ( 1 ) . trimmed ( ) ;
2012-03-31 11:20:19 -04:00
2012-09-21 17:23:14 -04:00
if ( ( flag & RSHTML_FORMATTEXT_REMOVE_FONT_FAMILY & & key = = " font-family " ) | |
( flag & RSHTML_FORMATTEXT_REMOVE_FONT_SIZE & & key = = " font-size " ) | |
( flag & RSHTML_FORMATTEXT_REMOVE_FONT_WEIGHT & & key = = " font-weight " ) | |
( flag & RSHTML_FORMATTEXT_REMOVE_FONT_STYLE & & key = = " font-style " ) ) {
continue ;
}
if ( flag & RSHTML_FORMATTEXT_REMOVE_COLOR ) {
if ( key = = " color " ) {
2012-03-31 11:20:19 -04:00
continue ;
}
}
2012-09-21 17:23:14 -04:00
else if ( flag & RSHTML_FORMATTEXT_FIX_COLORS ) {
2012-03-31 11:20:19 -04:00
if ( key = = " color " ) {
2012-09-21 17:23:14 -04:00
findBestColor ( val , bglum , desiredContrast ) ;
}
}
if ( flag & ( RSHTML_FORMATTEXT_REMOVE_COLOR | RSHTML_FORMATTEXT_FIX_COLORS ) ) {
if ( key = = " background " | | key = = " background-color " ) {
// Remove background color because if we change the text color,
// it can become unreadable on the original background.
// Also, FIX_COLORS is intended to display text on the default
// background color of the operating system.
2012-03-31 11:20:19 -04:00
continue ;
}
}
2012-09-21 17:23:14 -04:00
style + = key + " : " + val + " ; " ;
2012-03-31 11:20:19 -04:00
} else {
style + = pair + " ; " ;
}
}
}
}
if ( style . isEmpty ( ) ) {
node . attributes ( ) . removeNamedItem ( " style " ) ;
styleNode . clear ( ) ;
} else {
styleAttr . setValue ( style ) ;
}
2012-01-30 19:06:24 -05:00
}
2011-09-29 05:20:09 -04:00
if ( node . isElement ( ) ) {
QDomElement element = node . toElement ( ) ;
2012-01-30 19:06:24 -05:00
// not <p>
if ( addBR & & element . tagName ( ) . toLower ( ) ! = " p " ) {
// add <br> after a removed <p> but not before a <p>
QDomElement elementBr = doc . createElement ( " br " ) ;
currentElement . insertBefore ( elementBr , element ) ;
addBR = false ;
+ + index ;
}
// <body>
if ( element . tagName ( ) . toLower ( ) = = " body " ) {
if ( element . attributes ( ) . length ( ) = = 0 ) {
// remove <body> without attributes
removeElement ( currentElement , element ) ;
// no ++index;
continue ;
}
// change <body> to <span>
element . setTagName ( " span " ) ;
}
// <head>
2011-09-29 05:20:09 -04:00
if ( element . tagName ( ) . toLower ( ) = = " head " ) {
2012-01-30 19:06:24 -05:00
// remove <head>
2011-09-29 05:20:09 -04:00
currentElement . removeChild ( node ) ;
2012-01-30 19:06:24 -05:00
// no ++index;
2011-09-29 05:20:09 -04:00
continue ;
}
2012-01-30 19:06:24 -05:00
// iterate children
2012-09-21 17:23:14 -04:00
optimizeHtml ( doc , element , flag , bglum , desiredContrast ) ;
2012-01-30 19:06:24 -05:00
// <p>
if ( element . tagName ( ) . toLower ( ) = = " p " ) {
// <p style="...">
if ( element . attributes ( ) . size ( ) = = 1 & & styleNode . isAttr ( ) ) {
2012-05-04 19:39:36 -04:00
QString style = styleNode . toAttr ( ) . value ( ) . simplified ( ) ;
if ( style = = " margin:0px 0px 0px 0px;-qt-block-indent:0;text-indent:0px; " | |
style . startsWith ( " -qt-paragraph-type:empty;margin:0px 0px 0px 0px;-qt-block-indent:0;text-indent:0px; " ) ) {
2012-01-30 19:06:24 -05:00
if ( addBR ) {
// add <br> after a removed <p> before a removed <p>
QDomElement elementBr = doc . createElement ( " br " ) ;
currentElement . insertBefore ( elementBr , element ) ;
+ + index ;
}
// remove Qt standard <p> or empty <p>
index + = element . childNodes ( ) . length ( ) ;
removeElement ( currentElement , element ) ;
addBR = true ;
continue ;
2012-05-04 19:39:36 -04:00
}
// check for blockquote (not ready)
// style="margin-top:12px;margin-bottom:12px;margin-left:40px;margin-right:40px;-qt-block-indent:0;text-indent:0px;"
// int count = 0; // should be 6
// QStringList styles = style.split(';');
// foreach (QString pair, styles) {
// if (!pair.trimmed().isEmpty()) {
// QStringList keyvalue = pair.split(':');
// if (keyvalue.length() == 2) {
// QString key = keyvalue.at(0).trimmed();
// QString value = keyvalue.at(1).trimmed();
// if ((key == "margin-top" || key == "margin-bottom") && value == "12px") {
// ++count;
// continue;
// }
// if (key == "margin-left" || key == "margin-right") {
// ++count;
// continue;
// }
// if (key == "-qt-block-indent" && value == "0") {
// ++count;
// continue;
// }
// if (key == "text-indent" && value == "0px") {
// ++count;
// continue;
// }
// count = 0;
// break;
// } else {
// count = 0;
// break;
// }
// }
// }
// if (count == 6) {
// // change to "blockquote"
// element.setTagName("blockquote");
// element.attributes().removeNamedItem("style");
// element.setAttribute("type", "cite");
// }
2012-01-30 19:06:24 -05:00
}
addBR = false ;
}
2011-09-29 05:20:09 -04:00
}
+ + index ;
}
}
2012-05-11 20:40:53 -04:00
void RsHtml : : optimizeHtml ( QTextEdit * textEdit , QString & text , unsigned int flag )
2011-09-29 05:20:09 -04:00
{
if ( textEdit - > toHtml ( ) = = QTextDocument ( textEdit - > toPlainText ( ) ) . toHtml ( ) ) {
text = textEdit - > toPlainText ( ) ;
2013-03-19 17:45:13 -04:00
// std::cerr << "Optimized text to " << text.length() << " bytes , instead of " << textEdit->toHtml().length() << std::endl;
2011-09-29 05:20:09 -04:00
return ;
}
text = textEdit - > toHtml ( ) ;
2012-03-31 11:20:19 -04:00
optimizeHtml ( text , flag ) ;
2012-01-30 19:06:24 -05:00
}
2012-09-21 17:23:14 -04:00
/**
* @ brief Make an HTML document smaller by removing useless stuff .
* Can also change the text color to make it more readable .
* @ param [ in , out ] text HTML document .
* @ param flag Bitfield of RSHTML_FORMATTEXT_ * constants ( they must be part of
* RSHTML_OPTIMIZEHTML_MASK ) .
* @ param backgroundColor Background color of the widget where the text will be
* displayed . Needed only if RSHTML_FORMATTEXT_FIX_COLORS
* is passed inside flag .
* @ param desiredContrast Minimum contrast between text and background color ,
* between 1 and 21.
*/
void RsHtml : : optimizeHtml ( QString & text , unsigned int flag , const QColor & backgroundColor , qreal desiredContrast )
2012-01-30 19:06:24 -05:00
{
2013-06-10 09:27:24 -04:00
// int originalLength = text.length();
2012-01-30 19:06:24 -05:00
2011-09-29 05:20:09 -04:00
// remove doctype
text . remove ( QRegExp ( " <!DOCTYPE[^>]*> " ) ) ;
2014-01-18 17:55:14 -05:00
//remove all prepend char that make doc.setContent() fail
text . remove ( 0 , text . indexOf ( " < " ) ) ;
// Save Space and Tab because doc loose it.
text = saveSpace ( text ) ;
2011-09-29 05:20:09 -04:00
2014-01-18 17:55:14 -05:00
QString errorMsg ; int errorLine ; int errorColumn ;
2011-09-29 05:20:09 -04:00
QDomDocument doc ;
2014-01-18 17:55:14 -05:00
if ( doc . setContent ( text , & errorMsg , & errorLine , & errorColumn ) = = false ) {
2011-09-29 05:20:09 -04:00
return ;
}
QDomElement body = doc . documentElement ( ) ;
2012-09-21 17:23:14 -04:00
: : optimizeHtml ( doc , body , flag , : : getRelativeLuminance ( backgroundColor ) , desiredContrast ) ;
2011-09-29 05:20:09 -04:00
text = doc . toString ( - 1 ) ;
2013-03-19 17:45:13 -04:00
// std::cerr << "Optimized text to " << text.length() << " bytes , instead of " << originalLength << std::endl;
2011-09-29 05:20:09 -04:00
}
2012-05-11 20:40:53 -04:00
QString RsHtml : : toHtml ( QString text , bool realHtml )
2012-01-18 18:00:50 -05:00
{
// replace "\n" from the optimized html with "<br>"
text . replace ( " \n " , " <br> " ) ;
if ( ! realHtml ) {
return text ;
}
QTextDocument doc ;
doc . setHtml ( text ) ;
return doc . toHtml ( ) ;
}
2012-09-18 18:59:23 -04:00
/** Loads image and converts image to embedded image HTML fragment **/
bool RsHtml : : makeEmbeddedImage ( const QString & fileName , QString & embeddedImage , const int maxPixels )
{
QImage image ;
if ( image . load ( fileName ) = = false ) {
2015-03-27 14:41:44 -04:00
fprintf ( stderr , " RsHtml::makeEmbeddedImage() - image \" %s \" can't be load \n " , fileName . toLatin1 ( ) . constData ( ) ) ;
2012-09-18 18:59:23 -04:00
return false ;
}
return RsHtml : : makeEmbeddedImage ( image , embeddedImage , maxPixels ) ;
}
/** Converts image to embedded image HTML fragment **/
bool RsHtml : : makeEmbeddedImage ( const QImage & originalImage , QString & embeddedImage , const int maxPixels )
{
QByteArray bytearray ;
QBuffer buffer ( & bytearray ) ;
QImage resizedImage ;
const QImage * image = & originalImage ;
if ( maxPixels > 0 ) {
QSize imgSize = originalImage . size ( ) ;
if ( ( imgSize . height ( ) * imgSize . width ( ) ) > maxPixels ) {
// image is too large - resize keeping aspect ratio
QSize newSize ;
newSize . setWidth ( int ( qSqrt ( ( maxPixels * imgSize . width ( ) ) / imgSize . height ( ) ) ) ) ;
newSize . setHeight ( int ( ( imgSize . height ( ) * newSize . width ( ) ) / imgSize . width ( ) ) ) ;
// ask user
QMessageBox msgBox ;
2013-10-18 22:07:01 -04:00
msgBox . setText ( QString ( QApplication : : translate ( " RsHtml " , " Image is oversized for transmission. \n Reducing image to %1x%2 pixels? " ) ) . arg ( newSize . width ( ) ) . arg ( newSize . height ( ) ) ) ;
2012-09-18 18:59:23 -04:00
msgBox . setStandardButtons ( QMessageBox : : Ok | QMessageBox : : Cancel ) ;
msgBox . setDefaultButton ( QMessageBox : : Ok ) ;
if ( msgBox . exec ( ) ! = QMessageBox : : Ok ) {
return false ;
}
resizedImage = originalImage . scaled ( newSize , Qt : : KeepAspectRatio , Qt : : SmoothTransformation ) ;
image = & resizedImage ;
}
}
if ( buffer . open ( QIODevice : : WriteOnly ) ) {
if ( image - > save ( & buffer , " PNG " ) ) {
QByteArray encodedByteArray = bytearray . toBase64 ( ) ;
embeddedImage = " <img src= \" data:image/png;base64, " ;
embeddedImage . append ( encodedByteArray ) ;
embeddedImage . append ( " \" > " ) ;
} else {
2015-06-23 15:36:47 -04:00
//fprintf (stderr, "RsHtml::makeEmbeddedImage() - image can't be saved to buffer\n");
2012-09-18 18:59:23 -04:00
return false ;
}
} else {
2015-03-27 14:41:44 -04:00
fprintf ( stderr , " RsHtml::makeEmbeddedImage() - buffer can't be opened \n " ) ;
2012-09-18 18:59:23 -04:00
return false ;
}
return true ;
}
2013-07-01 16:35:07 -04:00
QString RsHtml : : plainText ( const QString & text )
{
2013-10-19 16:59:55 -04:00
# if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
return text . toHtmlEscaped ( ) ;
# else
2013-07-01 16:35:07 -04:00
return Qt : : escape ( text ) ;
2013-10-19 16:59:55 -04:00
# endif
2013-07-01 16:35:07 -04:00
}
QString RsHtml : : plainText ( const std : : string & text )
{
2013-10-19 16:59:55 -04:00
# if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
return QString : : fromUtf8 ( text . c_str ( ) ) . toHtmlEscaped ( ) ;
# else
2013-07-01 16:35:07 -04:00
return Qt : : escape ( QString : : fromUtf8 ( text . c_str ( ) ) ) ;
2013-10-19 16:59:55 -04:00
# endif
2013-07-01 16:35:07 -04:00
}