Commit 5136a867 authored by Enrico Ros's avatar Enrico Ros

Asyncronous PDF Generator implementation. The threaded pixmap generation

is working and features an hyperLock BUG due to my ignorance.
What to expect: more responsiveness from the UI, preloading, 'really'
continous scrolling, bubblegum and naked penguins.
Gonna hount insects now.

svn path=/branches/kpdf_experiments/kdegraphics/kpdf/; revision=373763
parent 92c653cf
......@@ -36,8 +36,8 @@
//BEGIN KPDFOutputDev
KPDFOutputDev::KPDFOutputDev( PDFGenerator * parent, SplashColor paperColor )
: SplashOutputDev( splashModeRGB8, false, paperColor ), m_pixmap( 0 ),
m_generator( parent ), m_text( 0 )
: SplashOutputDev( splashModeRGB8, false, paperColor ),
m_pixmap( 0 ), m_image( 0 ), m_generator( parent ), m_text( 0 )
{
}
......@@ -46,13 +46,14 @@ KPDFOutputDev::~KPDFOutputDev()
clear();
}
void KPDFOutputDev::setParams( int width, int height, bool genT, bool genL, bool genI )
void KPDFOutputDev::setParams( int width, int height, bool genT, bool genL, bool genI, bool safe )
{
clear();
m_pixmapWidth = width;
m_pixmapHeight = height;
m_qtThreadSafety = safe;
m_generateText = genT;
m_generateLinks = genL;
m_generateImages = genI;
......@@ -61,6 +62,16 @@ void KPDFOutputDev::setParams( int width, int height, bool genT, bool genL, bool
m_text = new TextPage( gFalse );
}
bool KPDFOutputDev::generateTextPage() const
{
return m_generateText;
}
bool KPDFOutputDev::generateRects() const
{
return m_generateLinks;
}
KPDFLink * KPDFOutputDev::generateLink( LinkAction * a )
{
......@@ -148,6 +159,13 @@ QPixmap * KPDFOutputDev::takePixmap()
return pix;
}
QImage * KPDFOutputDev::takeImage()
{
QImage * img = m_image;
m_image = 0;
return img;
}
TextPage * KPDFOutputDev::takeTextPage()
{
TextPage * text = m_text;
......@@ -157,11 +175,19 @@ TextPage * KPDFOutputDev::takeTextPage()
QValueList< KPDFPageRect * > KPDFOutputDev::takeRects()
{
if ( m_rects.isEmpty() )
return m_rects;
QValueList< KPDFPageRect * > rectsCopy( m_rects );
m_rects.clear();
return rectsCopy;
}
void KPDFOutputDev::freeInternalBitmap()
{
// ### hack: unload memory used by internal SplashOutputDev's bitmap
SplashOutputDev::startPage( 0, NULL );
}
void KPDFOutputDev::startPage( int pageNum, GfxState *state )
{
if ( m_generateText )
......@@ -175,21 +201,39 @@ void KPDFOutputDev::endPage()
if ( m_generateText )
m_text->coalesce( gTrue );
// create a QPixmap from page data
delete m_pixmap;
// create a QImage over the internally generated pixmap
int bh = getBitmap()->getHeight(),
bw = getBitmap()->getWidth();
SplashColorPtr dataPtr = getBitmap()->getDataPtr();
QImage * img = new QImage( (uchar*)dataPtr.rgb8, bw, bh, 32, 0, 0, QImage::IgnoreEndian );
// it may happen (in fact it doesn't) that we need rescaling
if ( bw != m_pixmapWidth || bh != m_pixmapHeight )
m_pixmap = new QPixmap( img->smoothScale( m_pixmapWidth, m_pixmapHeight ) );
else
m_pixmap = new QPixmap( *img );
delete img;
// ### hack: unload memory used by internal SplashOutputDev's bitmap
SplashOutputDev::startPage( 0, NULL );
// use the QImage or convert it immediately to QPixmap for better
// handling and memory unloading
if ( m_qtThreadSafety )
{
delete m_image;
m_image = img;
// it may happen (in fact it doesn't) that we need a rescaling
if ( bw != m_pixmapWidth && bh != m_pixmapHeight )
{
m_image = new QImage( img->smoothScale( m_pixmapWidth, m_pixmapHeight ) );
delete img;
}
m_image->detach();
// note: internal bitmap will be freed by PDFGenerator after getting the data
}
else
{
delete m_pixmap;
// it may happen (in fact it doesn't) that we need a rescaling
if ( bw != m_pixmapWidth || bh != m_pixmapHeight )
m_pixmap = new QPixmap( img->smoothScale( m_pixmapWidth, m_pixmapHeight ) );
else
m_pixmap = new QPixmap( *img );
delete img;
// free internal bitmap immediately
freeInternalBitmap();
}
}
void KPDFOutputDev::drawLink( Link * link, Catalog * catalog )
......@@ -290,6 +334,12 @@ void KPDFOutputDev::clear()
delete m_pixmap;
m_pixmap = 0;
}
// delete image
if ( m_image )
{
delete m_image;
m_image = 0;
}
// delete text
if ( m_text )
{
......
......@@ -46,17 +46,24 @@ class KPDFOutputDev : public SplashOutputDev
virtual ~KPDFOutputDev();
// to be called before PDFDoc->displayPage( thisclass, .. )
void setParams( int pixmapWidth, int pixmapHeight, bool generateTextpage,
bool decodeLinks, bool decodeImages );
// @param qtThreadSafety: use a slow QImage and conversions (for threads only)
void setParams( int pixmapWidth, int pixmapHeight, bool generateTextPage,
bool decodeLinks, bool decodeImages, bool qtThreadSafety = false );
bool generateTextPage() const;
bool generateRects() const;
// generate a valid KPDFLink subclass (or null) from a xpdf's LinkAction
KPDFLink * generateLink( LinkAction * );
// takes pointers out of the class (so deletion it's up to others)
QPixmap * takePixmap();
QImage * takeImage();
TextPage * takeTextPage();
QValueList< KPDFPageRect * > takeRects();
// free internal memory used by SplashOutputDev
void freeInternalBitmap();
/** inherited from OutputDev */
// Start a page.
virtual void startPage(int pageNum, GfxState *state);
......@@ -78,12 +85,14 @@ class KPDFOutputDev : public SplashOutputDev
void clear();
// generator switches and parameters
bool m_qtThreadSafety;
bool m_generateText;
bool m_generateLinks;
bool m_generateImages;
int m_pixmapWidth;
int m_pixmapHeight;
QPixmap * m_pixmap;
QImage * m_image;
PDFGenerator * m_generator;
// text page generated on demand
......
......@@ -6,27 +6,27 @@ Legend:
MRG - MeRGed from head
(*) - Some parts of this item are already done
In progress on the branch (first item comes first):
-> create a memory manager in Document with different profiles [90%]
Things to do in order to merge in HEAD (first item has highest priority):
**** 20041228 - READY TO MERGE ****
[ take care of naming on merge, too many differences (remove some kpdf_* prefixes
and rename internals too (document->kpdfdocument, page->kpdfpage, etc..)) ]
IN PROGRESS on the branch (first item comes first):
-> fix megaLock bug in PDF Threaded Pixmap Generator
-> sync Memory Management (in Document) with the Generator (exp. undoing requests)
-> add preloading
-> FIX: viewport changes the right way when clicking links and TOC items (also
suggested by Mikolaj Machowski). Create a great viewport definition and merge
it inside the synopsis too. [60% done]
Things to do in order to merge in HEAD (first item has highest priority):
-> take care of naming on merge, too differences (remove some kpdf_* prefixes
and rename internals too document->kpdfdocument, page->kpdfpage, etc..)
Higher priority after merge:
-> move toolbar view actions in the PageView instead of the part.. maybe.. or not...
More items (first items will enter 'In progress list' first):
-> move toolbar view actions in the PageView instead of the part. maybe.. or not...
-> link thumbnails view with document [first, the 'Viewport' must be defined]
-> usability: layout 2PPV [1 2,3 4,5 6] -> [1,2 3,4 5]. add option for 'ebook' style alignment. (by Mikolaj)
-> usability: trigger redraw on 'filter text' on current page (by Mikolaj)
-> async document generator using Albert's generator thread [0%]
-> watch out for FIXMEs in code
-> fix: requestPixmaps not triggered when exiting fullscreen mode (to verify..I can't reproduce)
More items (first items will enter 'In progress list' first):
*THIS ITEMS ARE CURRENTLY FROZEN SINCE "HEAD-MERGE" LIST IS CLEARED*
-> fix: iterate from older pages to newest ones when freeing memory (not randomly)
-> fix: On continous view mode, if you click to a link that moves you to another
page, then scroll up and click again on the same link it does not work. (by Albert)
......@@ -84,8 +84,9 @@ More items (first items will enter 'In progress list' first):
-> use wallet for storing passwords of encrypted files
Done (newest feature comes firts):
-> ADD: Asyncronous PDF Generator implementation (for the user: faster UI, preloading, etc..)
-> FIX: Memory manager (free cache if needed, avoid disk swap and oom)
-> ADD: Presentation View
-> ADD: Presentation View (only the 'glitter' transition implemented for now)
-> FIX: FixPack1 [dyn_zoom repaints, initial panel width, zoom_lineedit focus proxy, searchwidget refactor{thumbs restoring on clear, buttons size, less code}, hilight bookmarked thumbnails]
-> FIX: Some fullScreen loving, if we are on fullscreen put an action on RMB menu ti get out of it, if we were on fullScreen mode on exit bring back correctly if we were also seeing toolbar or menubar
-> FIX: When in non continous mode and scrolling up a page, set the viewport at the bottom of the page (Albert)
......
......@@ -553,7 +553,7 @@ void KPDFDocument::mCleanupMemory( int observerId )
++it;
}
}
kdDebug() << "Id:" << observerId << " [" << obs->totalMemory << "kB] Removed " << freed << " pages. " << obs->pageMemory.count() << " pages kept in memory." << endl;
//kdDebug() << "Id:" << observerId << " [" << obs->totalMemory << "kB] Removed " << freed << " pages. " << obs->pageMemory.count() << " pages kept in memory." << endl;
}
int KPDFDocument::mTotalMemory()
......
......@@ -10,6 +10,9 @@
// qt/kde includes
#include <qfile.h>
#include <qevent.h>
#include <qimage.h>
#include <qapplication.h>
#include <klocale.h>
#include <kpassdlg.h>
#include <kprinter.h>
......@@ -24,8 +27,6 @@
#include "xpdf/UnicodeMap.h"
#include "xpdf/Outline.h"
#include "goo/GList.h"
//#include "xpdf/PDFDoc.h"
//#include "xpdf/GlobalParams.h"
// local includes
#include "generator_pdf.h"
......@@ -34,25 +35,36 @@
#include "settings.h"
#include "QOutputDev.h"
// id for DATA_READY ThreadedGenerator Event
#define TGE_DATAREADY_ID 6969
PDFGenerator::PDFGenerator()
: pdfdoc( 0 ), kpdfOutputDev( 0 ), requestOnThread( 0 ),
: pdfdoc( 0 ), kpdfOutputDev( 0 ),
docInfoDirty( true ), docSynopsisDirty( true )
{
// generate kpdfOutputDev and cache page color
reparseConfig();
// generate the pixmapGeneratorThread
generatorThread = new PDFPixmapGeneratorThread( this );
}
PDFGenerator::~PDFGenerator()
{
// TODO wait for the end of threaded generator too and disable it
docLock.lock();
// first stop and delete the generator thread
if ( generatorThread )
{
generatorThread->wait();
if ( !generatorThread->isReady() )
generatorThread->endGeneration();
delete generatorThread;
}
// remove requests in queue
QValueList< PixmapRequest * >::iterator rIt = requestsQueue.begin(), rEnd = requestsQueue.end();
for ( ; rIt != rEnd; rIt++ )
delete *rIt;
requestsQueue.clear();
// remove other internal objects
docLock.lock();
delete kpdfOutputDev;
delete pdfdoc;
docLock.unlock();
......@@ -61,6 +73,13 @@ PDFGenerator::~PDFGenerator()
bool PDFGenerator::loadDocument( const QString & fileName, QValueVector<KPDFPage*> & pagesVector )
{
#ifndef NDEBUG
if ( pdfdoc )
{
kdDebug() << "PDFGenerator: multiple calls to loadDocument. Check it." << endl;
return false;
}
#endif
// create PDFDoc for the given file
pdfdoc = new PDFDoc( new GString( QFile::encodeName( fileName ) ), 0, 0 );
......@@ -111,6 +130,7 @@ const DocumentInfo * PDFGenerator::documentInfo()
{
if ( docInfoDirty )
{
docLock.lock();
// compile internal structure reading properties from PDFDoc
docInfo.author = getDocumentInfo("Author");
docInfo.creationDate = getDocumentDate("CreationDate");
......@@ -134,6 +154,7 @@ const DocumentInfo * PDFGenerator::documentInfo()
docInfo.encryption = i18n( "Unknown Encryption" );
docInfo.optimization = i18n( "Unknown Optimization" );
}
docLock.unlock();
// if pdfdoc is valid then we cached good info -> don't cache them again
if ( pdfdoc )
......@@ -246,14 +267,15 @@ bool PDFGenerator::print( KPrinter& printer )
void PDFGenerator::requestPixmap( PixmapRequest * request, bool syncronous )
{
//kdDebug() << "id: " << id << " is requesting pixmap for page " << page->number() << " [" << width << " x " << height << "]." << endl;
//kdDebug() << "id: " << request->id << " is requesting pixmap for page " << request->page->number() << " [" << request->width << " x " << request->height << "]." << endl;
// check if request has already been satisfied
KPDFPage * page = request->page;
if ( page->hasPixmap( request->id, request->width, request->height ) )
return;
if ( syncronous )
{
// in-place Pixmap generation for syncronous requests
KPDFPage * page = request->page;
if ( page->hasPixmap( request->id, request->width, request->height ) )
return;
// compute dpi used to get an image with desired width and height
double fakeDpiX = request->width * 72.0 / page->width(),
fakeDpiY = request->height * 72.0 / page->height();
......@@ -264,19 +286,25 @@ void PDFGenerator::requestPixmap( PixmapRequest * request, bool syncronous )
(request->height == page->height());
// generate links and image rects if rendering pages on pageview
bool genRects = request->id == PAGEVIEW_ID;
kpdfOutputDev->setParams( request->width, request->height, genTextPage, genRects, genRects );
// 0. LOCK [prevents thread from concurrent access]
docLock.lock();
// 1. Set OutputDev parameters and Generate contents
kpdfOutputDev->setParams( request->width, request->height, genTextPage, genRects, genRects );
pdfdoc->displayPage( kpdfOutputDev, page->number() + 1, fakeDpiX, fakeDpiY, 0, true, genRects );
docLock.unlock();
// 2. Take data from outputdev and attach it to the Page
page->setPixmap( request->id, kpdfOutputDev->takePixmap() );
if ( genTextPage )
page->setSearchPage( kpdfOutputDev->takeTextPage() );
if ( genRects )
page->setRects( kpdfOutputDev->takeRects() );
// pixmap generated
// 3. UNLOCK [re-enables shared access]
docLock.unlock();
// notify the new generation
contentsChanged( request->id, request->pageNumber );
// free memory (since we took ownership of the request)
......@@ -284,8 +312,29 @@ void PDFGenerator::requestPixmap( PixmapRequest * request, bool syncronous )
}
else
{
// check if an overlapping request is already stacked. if so remove
// the duplicated requests from the stack.
QValueList< PixmapRequest * >::iterator rIt = requestsQueue.begin();
QValueList< PixmapRequest * >::iterator rEnd = requestsQueue.end();
while ( rIt != rEnd )
{
const PixmapRequest * stackedRequest = *rIt;
if ( stackedRequest->id == request->id && stackedRequest->page == request->page )
{
delete stackedRequest;
rIt = requestsQueue.remove( rIt );
}
else
++rIt;
}
// queue the pixmap request at the top of the stack
requestsQueue.push_back( request );
//startThreadedGeneration();
// try to pop an item out of the stack and start generation. if
// a generation is already being done, the item just added will
// be processed in the 'event handler' calling this function.
startNewThreadedGeneration();
}
}
......@@ -295,9 +344,9 @@ void PDFGenerator::requestTextPage( KPDFPage * page )
KPDFTextDev td;
docLock.lock();
pdfdoc->displayPage( &td, page->number()+1, 72, 72, 0, true, false );
docLock.unlock();
// ..and attach it to the page
page->setSearchPage( td.takeTextPage() );
docLock.unlock();
}
bool PDFGenerator::reparseConfig()
......@@ -387,6 +436,7 @@ KPDFLinkGoto::Viewport PDFGenerator::decodeLinkViewport( GString * namedDest, Li
QString PDFGenerator::getDocumentInfo( const QString & data ) const
// note: this function is called by DocumentInfo gen, when the MUTEX is already LOCKED
{
// [Albert] Code adapted from pdfinfo.cc on xpdf
Object info;
......@@ -446,6 +496,7 @@ QString PDFGenerator::getDocumentInfo( const QString & data ) const
}
QString PDFGenerator::getDocumentDate( const QString & data ) const
// note: this function is called by DocumentInfo gen, when the MUTEX is already LOCKED
{
// [Albert] Code adapted from pdfinfo.cc on xpdf
Object info;
......@@ -489,109 +540,152 @@ QString PDFGenerator::getDocumentDate( const QString & data ) const
return i18n( "Unknown Date" );
}
/*
#include <qapplication.h>
#include <qevent.h>
#include <qimage.h>
void PDFGenerator::startNewThreadedGeneration()
{
// exit if already processing a request or queue is empty
if ( !generatorThread->isReady() || requestsQueue.isEmpty() )
return;
// if any requests are already accomplished, pop them from the stack
PixmapRequest * req = requestsQueue.last();
requestsQueue.pop_back();
if ( req->page->hasPixmap( req->id, req->width, req->height ) )
return startNewThreadedGeneration();
// start generator thread with given PixmapRequest
generatorThread->startGeneration( req );
}
ThumbnailGenerator::ThumbnailGenerator(PDFDoc *doc, QMutex *docMutex, int page, double ppp, QObject *o) : m_doc(doc), m_docMutex(docMutex), m_page(page), m_o(o), m_ppp(ppp)
void PDFGenerator::customEvent( QCustomEvent * event )
{
// catch generator responses only
if ( event->type() != TGE_DATAREADY_ID )
return;
// take data from the OutputDev
const PixmapRequest * request = generatorThread->currentRequest();
int reqId = request->id;
int reqPage = request->pageNumber;
// generate the QPixmap
QImage * outImage = kpdfOutputDev->takeImage();
QPixmap * pixmap = new QPixmap( *outImage );
delete outImage;
kpdfOutputDev->freeInternalBitmap();
// attach data to the Page
request->page->setPixmap( request->id, pixmap );
if ( kpdfOutputDev->generateTextPage() )
request->page->setSearchPage( kpdfOutputDev->takeTextPage() );
if ( kpdfOutputDev->generateRects() )
request->page->setRects( kpdfOutputDev->takeRects() );
// tell generator that contents has been taken and can unlock mutex
// note: request will be deleted so we can't access it from here on
generatorThread->endGeneration();
// notify the observer about changes in the page
contentsChanged( reqId, reqPage );
// proceed with generation
startNewThreadedGeneration();
}
int ThumbnailGenerator::getPage() const
/** The PDF Pixmap Generator Thread **/
PDFPixmapGeneratorThread::PDFPixmapGeneratorThread( PDFGenerator * gen )
: m_generator( gen ), m_currentRequest( 0 )
{
return m_page;
}
void ThumbnailGenerator::run()
PDFPixmapGeneratorThread::~PDFPixmapGeneratorThread()
{
QCustomEvent *ce;
//QImage *i;
SplashColor paperColor;
paperColor.rgb8 = splashMakeRGB8(0xff, 0xff, 0xff);
KPDFOutputDev odev(paperColor);
odev.startDoc(m_doc->getXRef());
m_docMutex->lock();
m_doc -> displayPage(&odev, m_page, m_ppp, m_ppp, 0, true, false);
m_docMutex->unlock();
ce = new QCustomEvent(65432);
//i = new QImage(odev.getImage());
//i -> detach();
//ce -> setData(i);
QApplication::postEvent(m_o, ce);
delete m_currentRequest;
}
*/
/** TO BE IMPORTED:
void generateThumbnails(PDFDoc *doc);
void stopThumbnailGeneration();
protected slots:
void customEvent(QCustomEvent *e);
private slots:
void changeSelected(int i);
void emitClicked(int i);
signals:
void clicked(int);
private:
void generateNextThumbnail();
ThumbnailGenerator *m_tg;
void resizeThumbnails();
int m_nextThumbnail;
bool m_ignoreNext;
DELETE:
if (m_tg)
void PDFPixmapGeneratorThread::startGeneration( PixmapRequest * request )
{
m_tg->wait();
delete m_tg;
#ifndef NDEBUG
// check if a generation is already running
if ( m_currentRequest )
{
kdDebug() << "PDFPixmapGeneratorThread: requesting a pixmap "
<< "when another is being generated." << endl;
delete request;
return;
}
// check if the mutex is already held
if ( m_generator->docLock.locked() )
{
kdDebug() << "PDFPixmapGeneratorThread: requesting a pixmap "
<< "with the mutex already held." << endl;
return;
}
#endif
// [LOCK] start locking XPDF thread unsafe classes
m_generator->docLock.lock();
// set generation parameters and run thread
m_currentRequest = request;
start( QThread::LowestPriority );
}
void ThumbnailList::generateThumbnails(PDFDoc *doc)
const PixmapRequest * PDFPixmapGeneratorThread::currentRequest() const
{
m_nextThumbnail = 1;
m_doc = doc;
generateNextThumbnail();
return m_currentRequest;
}
void ThumbnailList::generateNextThumbnail()
bool PDFPixmapGeneratorThread::isReady() const
{
if (m_tg)
{
m_tg->wait();
delete m_tg;
}
m_tg = new ThumbnailGenerator(m_doc, m_docMutex, m_nextThumbnail, QPaintDevice::x11AppDpiX(), this);
m_tg->start();
return !m_currentRequest;
}
void ThumbnailList::stopThumbnailGeneration()
void PDFPixmapGeneratorThread::endGeneration()
{
if (m_tg)
{
m_ignoreNext = true;
m_tg->wait();
delete m_tg;
m_tg = 0;
}
#ifndef NDEBUG
// check if a generation is already running
if ( !m_currentRequest )
{
kdDebug() << "PDFPixmapGeneratorThread: 'end generation' called "
<< "but generation was not started." << endl;
return;
}
#endif
// reset internal members preparing for a new generation
delete m_currentRequest;
m_currentRequest = 0;
// [UNLOCK] mutex
m_generator->docLock.unlock();
}
void ThumbnailList::customEvent(QCustomEvent *e)
void PDFPixmapGeneratorThread::run()
// perform contents generation, when the MUTEX is already LOCKED
// @see PDFGenerator::requestPixmap( .. ) (and be aware to sync the code)
{
if (e->type() == 65432 && !m_ignoreNext)
{
QImage *i = (QImage*)(e -> data());
setThumbnail(m_nextThumbnail, i);
m_nextThumbnail++;
if (m_nextThumbnail <= m_doc->getNumPages()) generateNextThumbnail();
else
{
m_tg->wait();