#include <QPicture>
#include <QPainter>
#include <QTextDocument>
#include <qtmmlwidget.h>
#include <stdexcept>
#include "lurchtextobject.h"
#include "lurchdocumentconverter.h"
#include "htmlutilities.h"
#include "lobscript.h"
#include "qtextdocumentutils.h"
#include "globaldefs.h"
const int LTOFK_STRING = LTO_FORMAT_KEY + 1;
QCache<QString,QPicture> LurchTextObject::cache;
QMap<QString,float> LurchTextObject::percentBelowBaseline;
LurchTextObject::LurchTextObject ( QFont defaultFont )
: env( NULL ), magFactor( 1.0 ), lastScaleFactor( -1 )
{
mathRenderer = new QtMmlWidget();
mathRenderer->setBackgroundRole( QPalette::Base );
svgRenderer = new QSvgRenderer( this );
documentFont = defaultFont;
}
LurchTextObject::~LurchTextObject ()
{
delete mathRenderer;
}
void LurchTextObject::setEnvironment ( LurchEnvironment* environment )
{
env = environment;
}
void LurchTextObject::setMagnification ( qreal factor )
{
magFactor = factor;
}
int LurchTextObject::descent ( const QTextFormat &format, int fullImageHeight,
bool includeMagnificationFactor )
{
QString str = formatToString( format );
float descentPercent = percentBelowBaseline.contains( str ) ?
percentBelowBaseline[str] : 0.0;
if ( fullImageHeight == -1 )
fullImageHeight = formatToPicture( format ).height();
double result = fullImageHeight * descentPercent;
if ( includeMagnificationFactor ) result *= magFactor;
return (int)result;
}
QSizeF LurchTextObject::intrinsicSize ( QTextDocument* /*doc*/, int /*posInDocument*/,
const QTextFormat &format )
{
QPicture picture = formatToPicture( format );
int desc = descent( format, picture.height() );
return ( QSize( picture.boundingRect().width(),
picture.boundingRect().height() ) * magFactor ) - QSize( 0, desc );
}
void LurchTextObject::drawObject( QPainter *painter, const QRectF &rect,
QTextDocument* /*doc*/, int /*posInDocument*/,
const QTextFormat &format )
{
painter->save();
painter->translate( rect.topLeft() );
painter->scale( magFactor, magFactor );
QPicture p = formatToPicture( format );
p.play( painter );
painter->restore();
}
bool LurchTextObject::updateLobRepresentation ( const Lob& L, QTextDocument* document,
bool force )
{
QTextNode frag = QTextNode( document->rootFrame() ).index( L.address() );
if ( !frag.isFragment() ) {
// qDebug() << "This was supposed to be a smart character:" << frag.toString();
return false;
}
QTextCharFormat fmt = frag.asFragment().charFormat();
QString newRep = formatToString( fmt, true );
QString oldRep = fmt.property( LTOFK_STRING ).value<QString>();
if ( force || ( newRep != oldRep ) ) {
fmt.setProperty( LTOFK_STRING, newRep );
frag.setFormat( fmt );
// qDebug() << "Updated representation of" << L.address().toString() << "to this:" << newRep;
return true;
}
// qDebug() << "Did not need to update" << L.address().toString();
return false;
}
QString LurchTextObject::formatToHTML ( const QTextFormat& format )
{
QString stringRep = formatToString( format );
if ( isDataURI( stringRep ) )
return "<img src=\"" + stringRep + "\"/>";
if ( stringRep.startsWith( "$" ) && stringRep.endsWith( "$" ) )
return "\\(" + stringRep.mid( 1, stringRep.length() - 2 ) + "\\)";
if ( stringRep.startsWith( "<html>" ) && stringRep.endsWith( "</html>" ) )
stringRep = stringRep.mid( 6, stringRep.length() - 13 );
QTextDocument doc;
doc.setDocumentMargin( 2 );
doc.setDefaultFont( QFont( DEFAULT_FIXED_WIDTH_FONT ) );
int pix = QFontInfo( doc.defaultFont() ).pixelSize();
QRegExp imgTag( "<img\\s+([^>]+)>" );
QString tag;
for ( int pos = 0 ; ; pos += tag.length() ) {
pos = stringRep.indexOf( imgTag, pos );
if ( pos == -1 )
break;
QString prefix = stringRep.left( pos );
QString suffix = stringRep.mid( pos + imgTag.matchedLength() );
tag = imgTag.cap();
QRegExp srcval( "\\s*src\\s*=\\s*('[^']*'|\"[^\"]*\")" );
if ( srcval.indexIn( tag ) == -1 )
continue;
QString src = srcval.cap( 1 ).mid( 1, srcval.cap( 1 ).length() - 2 );
QPixmap img;
if ( isDataURI( src ) ) {
img = dataURIToImage( src );
} else {
img.load( src );
tag = tag.replace( src, createDataURI( img ) );
}
QRegExp pszval( "\\s*pixelsize\\s*=\\s*('[^']*'|\"[^\"]*\"|[0-9]+)" );
if ( pszval.indexIn( tag ) > -1 ) {
QString psz = pszval.cap( 1 );
if ( psz.startsWith( "'" ) || psz.startsWith( "\"" ) )
psz = psz.mid( 1, psz.length() - 2 );
int pszint = psz.toInt();
QSize newsize = img.size();
if ( pszint ) {
newsize *= pix;
newsize /= pszint;
}
tag = tag.replace( pszval.cap(),
QString( " width=\"%1\" height=\"%2\"" )
.arg( newsize.width() ).arg( newsize.height() ) );
}
stringRep = prefix + tag + suffix;
}
return stringRep;
}
QString LurchTextObject::formatToString ( const QTextFormat& format, bool forceRecompute )
{
// if a string representation is pre-computed, use it
if ( ( !forceRecompute || env->engine()->isEvaluating() )
&& format.hasProperty( LTOFK_STRING ) )
return format.property( LTOFK_STRING ).value<QString>();
// otherwise, compute the string representation all over again, of this thing:
Lob L = LurchDocumentConverter::getLobFrom( format.toCharFormat() );
// if the Lob has an attribute that explicitly gives its representation, use that:
Lob R = L.attribute( "representation", "LurchUI" );
if ( !R.isEmpty() )
return R.toVariant().toString();
// if we have no environment, or no way to handle representation calls, or the engine is
// busy, return an empty representation for now
// (it's important to not run this if another script is running, because otherwise it may
// try to compute representations before the document is even fully loaded, causing script
// errors and even application crashes)
if ( ( env == NULL ) || !env->engine()->globalObject().property( "handle" ).isValid()
|| env->engine()->isEvaluating() )
return "<html></html>";
// we are able to run the script to see if the document knows how to compute the
// representation of this Lob, so do so
// if ( env->document().index( L.address() ) != L )
// qDebug() << "\n\nSYNC PROBLEM:\n" << L.toString() << "\n";
// else
// qDebug() << "\nsyncing is just fine for" << L.address().toString();
QScriptValue attempt = env->engine()->globalObject().property( "handle" ).call(
QScriptValue(), QScriptValueList()
<< env->engine()->toScriptValue( QString( "representation" ) )
<< env->engine()->toScriptValue( L ) );
// return the value computed, or if the computation failed, return a placeholder that makes
// it clear that there is a Lob of unknown type present in the document:
return attempt.toBool() ? attempt.toString()
: "<html><font color='#ff8800'><b>[?]</b></font></html>";
}
QPicture LurchTextObject::formatToPicture ( const QTextFormat& format )
{
// if the scaling factor has changed since the last time anything was rendered,
// then remove all typeset math from the cache:
int intScaleFactor = QSettings().value( "main/typeset_math_scale_factor", 100 ).toInt();
if ( ( lastScaleFactor > -1 ) && ( intScaleFactor != lastScaleFactor ) ) {
QStringList keys = cache.keys();
foreach ( QString key, keys ) {
QStringList bits = key.split( " " );
if ( ( bits.count() >= 3 )
&& bits[2].startsWith( "$" ) && bits.last().endsWith( "$" ) )
cache.remove( key );
}
}
lastScaleFactor = intScaleFactor;
// if a picture for the string representation already exists, use it
QString stringRep = formatToString( format );
QString key = QString( "%1 %2 %3" )
.arg( format.toCharFormat().font().pointSizeF() )
.arg( LurchDocumentConverter::charFormatToString( format.toCharFormat() ) )
.arg( stringRep );
if ( cache.contains( key ) )
return *cache[key];
// otherwise, compute the picture from the string representation and cache it for later
// eventually this will need to decode different types of representation
// (HTML, base64 image, TeX, etc.), but for now:
QPicture picture;
// empty html is treated as an invisible text object
if ( stringRep.toUpper() == "<HTML></HTML>" ) {
picture.setBoundingRect( QRect( 0, 0, 0, 0 ) );
return picture;
}
// if they sent (non-empty) HTML, it must be of the form "<html>...</html>"
if ( ( stringRep.left( 6 ).toUpper() == "<HTML>" )
&& ( stringRep.right( 7 ).toUpper() == "</HTML>" ) ) {
QTextDocument doc;
doc.setDocumentMargin( 2 );
doc.setDefaultFont( QFont( DEFAULT_FIXED_WIDTH_FONT ) );
int pix = QFontInfo( doc.defaultFont() ).pixelSize();
QRegExp imgTag( "<img\\s+([^>]+)>" );
QString tag;
for ( int pos = 0 ; ; pos += tag.length() ) {
pos = stringRep.indexOf( imgTag, pos );
if ( pos == -1 )
break;
QString prefix = stringRep.left( pos );
QString suffix = stringRep.mid( pos + imgTag.matchedLength() );
tag = imgTag.cap();
QRegExp srcval( "\\s*src\\s*=\\s*('[^']*'|\"[^\"]*\")" );
if ( srcval.indexIn( tag ) == -1 )
continue;
QString src = srcval.cap( 1 ).mid( 1, srcval.cap( 1 ).length() - 2 );
QPixmap img;
if ( isDataURI( src ) )
img = dataURIToImage( src );
else
img.load( src );
QRegExp pszval( "\\s*pixelsize\\s*=\\s*('[^']*'|\"[^\"]*\"|[0-9]+)" );
if ( pszval.indexIn( tag ) == -1 )
continue;
QString psz = pszval.cap( 1 );
if ( psz.startsWith( "'" ) || psz.startsWith( "\"" ) )
psz = psz.mid( 1, psz.length() - 2 );
int pszint = psz.toInt();
QSize newsize = img.size();
if ( pszint ) {
newsize *= pix;
newsize /= pszint;
}
tag = tag.replace( pszval.cap(),
QString( " width=\"%1\" height=\"%2\"" )
.arg( newsize.width() ).arg( newsize.height() ) );
stringRep = prefix + tag + suffix;
}
doc.setHtml( stringRep.mid( 6, stringRep.length() - 13 ) );
QPainter p( &picture );
doc.drawContents( &p );
QSize size = doc.size().toSize();
if ( !size.isEmpty() ) {
picture.setBoundingRect( QRect( QPoint( 0, 2 ), size + QSize( 0, -2 ) ) );
cache.insert( key, new QPicture( picture ) );
return picture;
} else {
stringRep = "[empty HTML]";
}
}
// if they sent a data URI for an image, it must be of the form
// "data:image/FORMAT_HERE;base64,BASE_64_ENCODING_HERE"
if ( isDataURI( stringRep ) ) {
QPixmap decoded = dataURIToImage( stringRep );
if ( !decoded.size().isEmpty() ) {
QPainter p( &picture );
p.drawPixmap( 0, 0, decoded );
picture.setBoundingRect( QRect( QPoint( 0, 0 ), decoded.size() ) );
cache.insert( key, new QPicture( picture ) );
return picture;
} else {
stringRep = "[empty image]";
}
}
// if they sent a TeX string, it must be of the form "$...$"
if ( stringRep.startsWith( "$" ) && stringRep.endsWith( "$" ) ) {
if ( !te.hasComputed( "8" ) ) {
// enqueue "x" for background TeX rendering, for height comparison purposes
te.asyncTeX2SVG( "8" );
emit deferredTypesetting();
picture.setBoundingRect( QRect( 0, 0, 0, 0 ) );
return picture;
}
QString eight = te.TeX2SVG( "8" );
svgRenderer->load( eight.toUtf8() );
double typesetHeight = svgRenderer->defaultSize().height();
// now we know the size of an x, so we can compare other things to it...
QString forTE = unescapeXML( stringRep );
if ( !te.hasComputed( forTE ) ) {
// enqueue for background TeX rendering, and return empty picture for now
te.asyncTeX2SVG( forTE );
emit deferredTypesetting();
picture.setBoundingRect( QRect( 0, 0, 0, 0 ) );
return picture;
}
// it was already background computed some other time, so fetch the result
QString svgCode = te.TeX2SVG( forTE );
svgRenderer->load( svgCode.toUtf8() );
QSizeF def = svgRenderer->defaultSize();
QRegExp emptyTeX( "\\$\\$?\\s*\\$\\$?" );
if ( ( def.width() <= 0 ) && !emptyTeX.exactMatch( stringRep ) ) {
forTE = "\\bbox[2pt,border:1px solid red]{\\text{Invalid }\\TeX}";
if ( !te.hasComputed( forTE ) ) {
// enqueue for background TeX rendering, and return empty picture for now
te.asyncTeX2SVG( forTE );
emit deferredTypesetting();
picture.setBoundingRect( QRect( 0, 0, 0, 0 ) );
return picture;
}
// it was already background computed some other time, so fetch the result
svgRenderer->load( te.TeX2SVG( forTE ).toUtf8() );
def = svgRenderer->defaultSize();
}
QRectF vb = svgRenderer->viewBoxF();
float percentBelow = vb.height() ? ( vb.height() + vb.y() ) / vb.height() : 0.0;
percentBelowBaseline[stringRep] = percentBelow;
// testing indicated that 92% of normal size looked best with our default fonts:
float scaleFactor = intScaleFactor * 0.0092;
float normalHeight = QFontInfo( documentFont ).pixelSize() * scaleFactor;
float ratio = typesetHeight ? ( normalHeight / typesetHeight ) : 1;
QRect bounds( QPoint( 0, 0 ), QSize( def.width()*ratio, def.height()*ratio ) );
picture.setBoundingRect( bounds );
QPainter p( &picture );
svgRenderer->render( &p, bounds );
cache.insert( key, new QPicture( picture ) );
return picture;
}
// if they sent a MathML string, it must be of the form "<mathml>...</mathml>"
if ( stringRep.startsWith( "<mathml>" ) && stringRep.endsWith( "</mathml>" ) ) {
QString errorMessage;
int error_line, error_column;
bool result = mathRenderer->setContent( stringRep.mid( 8, stringRep.length() - 17 ),
&errorMessage, &error_line, &error_column );
if ( !result )
errorMessage = QString( "[MathML error:%2:%3:%1]" )
.arg( errorMessage ).arg( error_line ).arg( error_column );
if ( errorMessage.isEmpty() ) {
mathRenderer->resize( mathRenderer->sizeHint() );
mathRenderer->render( &picture );
picture.setBoundingRect( QRect( 0, 0,
mathRenderer->width(), mathRenderer->height() ) );
cache.insert( key, new QPicture( picture ) );
return picture;
} else {
stringRep = errorMessage;
}
}
// anything else is treated as plain text
QPainter p( &picture );
QRectF limits = p.boundingRect( QRectF( 0, 0, 500, 300 ), stringRep );
picture.setBoundingRect( limits.toRect() );
p.drawText( limits, stringRep );
cache.insert( key, new QPicture( picture ) );
return picture;
}