[go: up one dir, main page]

Menu

[80c337]: / lurchtextobject.cpp  Maximize  Restore  History

Download this file

373 lines (353 with data), 16.3 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#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;
}