/*
* This file is part of NixNote
* Copyright 2009 Randy Baumgarte
*
* This file may be licensed under the terms of of the
* GNU General Public License Version 2 (the ``GPL'').
*
* Software distributed under the License is distributed
* on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
* express or implied. See the GPL for the specific language
* governing rights and limitations.
*
* You should have received a copy of the GPL along with this
* program. If not, go to http://www.gnu.org/licenses/gpl.html
* or write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package cx.fbn.nevernote.threads;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import com.evernote.edam.type.Note;
import com.trolltech.qt.core.QBuffer;
import com.trolltech.qt.core.QByteArray;
import com.trolltech.qt.core.QIODevice;
import com.trolltech.qt.core.QMutex;
import com.trolltech.qt.core.QObject;
import com.trolltech.qt.core.QTemporaryFile;
import com.trolltech.qt.gui.QPixmap;
import cx.fbn.nevernote.Global;
import cx.fbn.nevernote.signals.NoteSignal;
import cx.fbn.nevernote.sql.DatabaseConnection;
import cx.fbn.nevernote.utilities.ApplicationLogger;
import cx.fbn.nevernote.xml.NoteFormatter;
/*
*
* @author Randy Baumgarte
*
* Thumbnail Overview:
*
* How thumbnails are generated is a bit odd. The problem is that
* process of creating the thumbnail involves actually creating an HTML
* version of the note & all of its resources. That is very CPU intensive
* so we try to do it in a separate thread. Unfortunately, the QWebPage class
* which actually creates the thumbnail must be in the main GUI thread.
* This is the odd way I've tried to get around the problem.
*
* First, the thumbail thread finds a note which needs a thumbnail. This
* can be done by either scanning the database or specifically being told
* a note needs a new thumbnail.
*
* When a note is found, this thread will read the database and write out
* the resources and create an HTML version of the note. It then signals
* the main GUI thread that a note is ready.
*
* Next, the main GUI thread will process the signal received from the
* thumbnail thread. The GUI thread will create a QWebPage (via the
* Thumbnailer class) and will render the image. The image is written to
* the database to be used in the thumbnail view.
*
*/
public class ThumbnailRunner extends QObject implements Runnable {
private final ApplicationLogger logger;
private String guid;
public NoteSignal noteSignal;
private boolean keepRunning;
public boolean interrupt;
private final DatabaseConnection conn;
private volatile LinkedBlockingQueue<String> workQueue;
private static int MAX_QUEUED_WAITING = 1000;
public QMutex mutex;
public ThumbnailRunner(String logname, String u, String i, String r, String uid, String pswd, String cpswd) {
logger = new ApplicationLogger(logname);
conn = new DatabaseConnection(logger, u, i, r, uid, pswd, cpswd, 300);
noteSignal = new NoteSignal();
guid = null;
keepRunning = true;
mutex = new QMutex();
workQueue=new LinkedBlockingQueue<String>(MAX_QUEUED_WAITING);
}
@Override
public void run() {
thread().setPriority(Thread.MIN_PRIORITY);
logger.log(logger.MEDIUM, "Starting thumbnail thread ");
while (keepRunning) {
try {
interrupt = false;
String work = workQueue.take();
if (work.startsWith("GENERATE")) {
work = work.replace("GENERATE ", "");
guid = work;
generateThumbnail();
}
if (work.startsWith("SCAN")) {
if (conn.getNoteTable().getThumbnailNeededCount() > 1)
scanDatabase();
}
if (work.startsWith("IMAGE")) {
work = work.replace("IMAGE ", "");
guid = work;
processImage();
}
if (work.startsWith("STOP")) {
logger.log(logger.MEDIUM, "Stopping thumbail thread");
keepRunning = false;
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
conn.dbShutdown();
}
private void processImage() {
boolean abort = true;
if (abort)
return;
mutex.lock();
logger.log(logger.EXTREME, "Image found "+guid);
logger.log(logger.EXTREME, "Getting image");
QPixmap image = new QPixmap();
if (!image.load(Global.getFileManager().getResDirPath()+"thumbnail-"+guid+".png")) {
logger.log(logger.EXTREME, "Failure to reload image. Aborting.");
mutex.unlock();
return;
}
logger.log(logger.EXTREME, "Opening buffer");
QBuffer buffer = new QBuffer();
if (!buffer.open(QIODevice.OpenModeFlag.WriteOnly)) {
logger.log(logger.EXTREME, "Failure to open buffer. Aborting.");
mutex.unlock();
return;
}
logger.log(logger.EXTREME, "Filling buffer");
if (!image.save(buffer, "PNG")) {
logger.log(logger.EXTREME, "Failure to write to buffer. Aborting.");
mutex.unlock();
return;
}
buffer.close();
logger.log(logger.EXTREME, "Updating database");
QByteArray b = new QBuffer(buffer).buffer();
conn.getNoteTable().setThumbnail(guid, b);
conn.getNoteTable().setThumbnailNeeded(guid, false);
mutex.unlock();
}
private void scanDatabase() {
// If there is already work in the queue, that takes priority
logger.log(logger.HIGH, "Scanning database for notes needing thumbnail");
if (workQueue.size() > 0)
return;
// Find a few records that need thumbnails
List<String> guids = conn.getNoteTable().findThumbnailsNeeded();
logger.log(logger.HIGH, guids.size() +" records returned");
for (int i=0; i<guids.size() && keepRunning && !interrupt; i++) {
guid = guids.get(i);
logger.log(logger.HIGH, "Working on:" +guids.get(i));
generateThumbnail();
}
logger.log(logger.HIGH, "Scan completed");
}
public synchronized boolean addWork(String request) {
if (workQueue.size() == 0) {
workQueue.offer(request);
return true;
}
return false;
}
public synchronized int getWorkQueueSize() {
return workQueue.size();
}
private void generateThumbnail() {
QByteArray js = new QByteArray();
logger.log(logger.HIGH, "Starting thumbnail for " +guid);
ArrayList<QTemporaryFile> tempFiles = new ArrayList<QTemporaryFile>();
Note currentNote = conn.getNoteTable().getNote(guid,true,true,false,true,false);
NoteFormatter formatter = new NoteFormatter(logger, conn, tempFiles);
currentNote = conn.getNoteTable().getNote(guid, true, true, false, true, false);
formatter.setNote(currentNote, true);
formatter.setHighlight(null);
js.append("<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">");
js.append("<style type=\"text/css\">.en-crypt-temp { border-collapse:collapse; border-style:solid; border-color:blue; padding:0.0mm 0.0mm 0.0mm 0.0mm; }</style>");
js.append("<style type=\"text/css\">en-hilight { background-color: rgb(255,255,0) }</style>");
js.append("<style> img { max-width:100%; }</style>");
js.append("<style type=\"text/css\">en-spell { text-decoration: none; border-bottom: dotted 1px #cc0000; }</style>");
js.append("</head>");
js.append(formatter.rebuildNoteHTML());
js.append("</HTML>");
js.replace("<!DOCTYPE en-note SYSTEM 'http://xml.evernote.com/pub/enml.dtd'>", "");
js.replace("<!DOCTYPE en-note SYSTEM 'http://xml.evernote.com/pub/enml2.dtd'>", "");
js.replace("<?xml version='1.0' encoding='UTF-8'?>", "");
int zoom = 1;
if (currentNote != null && currentNote.getContent() != null) {
String content = currentNote.getContent();
zoom = Global.calculateThumbnailZoom(content);
}
logger.log(logger.HIGH, "Thumbnail file ready");
noteSignal.thumbnailPageReady.emit(guid, js, zoom);
}
}