mirror of
https://invent.kde.org/graphics/okular
synced 2024-11-05 18:34:53 +00:00
3832a6522f
Differential Revision: https://phabricator.kde.org/D17801
907 lines
31 KiB
Text
907 lines
31 KiB
Text
/**
|
|
\mainpage Okular, the unified document viewer
|
|
|
|
\section okular_overview Overview
|
|
|
|
- \ref okular_history
|
|
- \ref okular_design
|
|
- \ref okular_generators
|
|
- <a href="http://www.okular.org">Website</a>
|
|
|
|
\authors Tobias König <tokoe@kde.org>
|
|
|
|
\licenses \lgpl
|
|
|
|
\page okular_history Historical background
|
|
|
|
Okular is the successor of <a href="http://kpdf.kde.org">kpdf</a>, the PDF viewer in KDE 3.
|
|
kpdf was refactored and extended in a Google Summer of Code project to support not only
|
|
viewing PDF but also other types of document, e.g. PostScript files, images and many more.
|
|
|
|
\page okular_design The Design of Okular
|
|
|
|
To support a wide range of document formats, Okular was designed in a modular way, so you
|
|
have the following components:
|
|
|
|
\li \ref Shell
|
|
\li \ref Okular::Part
|
|
\li \ref Okular::Document Class
|
|
\li \ref Okular::Generator
|
|
|
|
The shell is the application which is started by the user as standalone application and
|
|
which embeds the part. The part contains all GUI elements of Okular, for example the
|
|
content list, the bookmark manager, menus and the graphical view of the document class.
|
|
The document class is an abstract presentation of the document content. It contains information
|
|
about every page of the document, its size, orientation etc.
|
|
|
|
But somehow the document class must retrieve these information from the various types of documents.
|
|
This is the task of the Generators. Generators are plugins which are loaded at runtime and which
|
|
have the knowledge about the internal structure of the different document types.
|
|
They extract the needed information from the documents, convert the data into a common format and
|
|
pass them to the document class.
|
|
|
|
Currently Generators for the following document types are available:
|
|
|
|
\li Portable Document Format (PDF)
|
|
\li PostScript
|
|
\li Device Independent Format (DVI)
|
|
\li DeJaVu Format
|
|
\li Comic Books
|
|
\li Images (JPEG, PNG, GIF, and many more)
|
|
\li TIFF Image Format
|
|
\li FictionBook Format
|
|
\li Plucker Format
|
|
\li OpenDocument Text Format
|
|
\li Microsoft's CHM Format
|
|
\li Microsoft's XML Document Format
|
|
\li Markdown Format
|
|
|
|
Now the questions is how can these various formats be represented in a unified way?
|
|
Okular provides features like rotation, text search and extraction, zooming and many more, so how
|
|
does it match with the different capabilities of the formats?
|
|
|
|
\section okular_design_basics Basics of Generators
|
|
|
|
Lets start with the smallest commonness of all document formats:
|
|
|
|
\li they have pages (one ore more) of a given size
|
|
\li pages can be represented as pictures
|
|
|
|
So the first thing every Generator must support is to return the number of pages of a document.
|
|
Furthermore it must be able to return the picture of a page at a requested size.
|
|
|
|
For vector based document formats (e.g. PDF or PostScript) the Generators can render the page for
|
|
the requested size, for static documents formats (e.g. images), the Generator must scale the
|
|
content according to the requested size, so when you zoom a page in Okular, the Generators are
|
|
just asked to return the page for the zoomed size.
|
|
|
|
When the document class has retrieved the page pictures from the Generators, it can do further
|
|
image manipulation on it, for example rotating them or applying fancy effects.
|
|
|
|
\section okular_design_text_support Generators with Text support
|
|
|
|
Some document formats however support more functionality than just representing a page as an image.
|
|
PDF, PostScript, DVI and DeJaVu for example contains a machine readable representation of the
|
|
included text. For those document formats Okular provides additional features like text search,
|
|
text extraction and text selection.
|
|
|
|
How is that supported by the Generators?
|
|
|
|
To access the text from the documents the generators must extract it somehow and make it available
|
|
to the document class. However for the text selection feature the document class must also know <em>where</em>
|
|
the extracted text is located on the page. For a zoom factor of 100% the absolute position of
|
|
the text in the document can be used, however for larger or smaller zoom factors the position
|
|
must be recalculated. To make this calculation as easy as possible, the Generators return an
|
|
abstract representation (\ref Okular::TextPage) of the text which includes every character together
|
|
with its <em>normalized</em> position. Normalized means that the width and height of the page is
|
|
in the range of 0 to 1, so a character in the middle of the page is at x=0.5 and y=0.5.
|
|
|
|
So when you want to know where this character is located on the page which is zoomed at 300%, you just
|
|
multiply the position by 3 * page width (and page height) and get the absolute position for this zoom level.
|
|
|
|
This abstract text representation also allows an easy rotation of the coordinates, so that text selection
|
|
is available on rotated pages as well.
|
|
|
|
\section okular_design_meta_information Meta Information
|
|
|
|
Most documents have additional meta information:
|
|
|
|
\li Name of the author
|
|
\li Date of creation
|
|
\li Version number
|
|
\li Table of Content
|
|
\li Bookmarks
|
|
\li Annotations
|
|
|
|
These information can be retrieved by the generator as well and will be shown by Okular.
|
|
|
|
\page okular_generators How to implement a Generator
|
|
|
|
The power of Okular is its extensibility by Generator plugins. This section will describe how to
|
|
implement your own plugin for a new document type.
|
|
|
|
\li \ref okular_generators_basic
|
|
\li \ref okular_generators_with_text
|
|
\li \ref okular_generators_threaded
|
|
\li \ref okular_generators_extended
|
|
|
|
\section okular_generators_basic A Basic Generator
|
|
|
|
To provide a short overview and don't reimplementing an existing generator we'll work on a Generator
|
|
for the Magic document format, a non existing, pure virtual format :)
|
|
|
|
Lets assume we have some helper class (MagicDocument) which provides the following functionality for this
|
|
document format:
|
|
|
|
\li Loading a document
|
|
\li Retrieving number of pages
|
|
\li Returning a fixed size picture representation of a page
|
|
|
|
The class API looks like this
|
|
|
|
\code
|
|
class MagicDocument
|
|
{
|
|
public:
|
|
MagicDocument();
|
|
~MagicDocument();
|
|
|
|
bool loadDocument( const QString &fileName );
|
|
|
|
int numberOfPages() const;
|
|
|
|
QSize pageSize( int pageNumber ) const;
|
|
|
|
QImage pictureOfPage( int pageNumber ) const;
|
|
|
|
private:
|
|
...
|
|
};
|
|
\endcode
|
|
|
|
The methods should be self explaining, loadDocument() loads a document file and returns false on error,
|
|
numberOfPages() returns the number of pages, pageSize() returns the size of the page and pictureOfPage()
|
|
returns the picture representation of the page.
|
|
|
|
Our first version of our Generator is a basic one which just provides page pictures to the document class.
|
|
|
|
The API of the Generator looks like the following:
|
|
|
|
\code
|
|
#include "magicdocument.h"
|
|
|
|
#include <okular/core/generator.h>
|
|
|
|
class MagicGenerator : public Okular::Generator
|
|
{
|
|
public:
|
|
MagicGenerator( QObject *parent, const QVariantList &args );
|
|
~MagicGenerator();
|
|
|
|
bool loadDocument( const QString &fileName, QVector<Okular::Page*> &pages );
|
|
|
|
bool canGeneratePixmap() const;
|
|
void generatePixmap( Okular::PixmapRequest *request );
|
|
|
|
protected:
|
|
bool doCloseDocument();
|
|
|
|
private:
|
|
MagicDocument mMagicDocument;
|
|
};
|
|
\endcode
|
|
|
|
The implementation of the Generator looks like this:
|
|
|
|
\code
|
|
#include <okular/core/page.h>
|
|
|
|
#include "magicgenerator.h"
|
|
|
|
OKULAR_EXPORT_PLUGIN(MagicGenerator, "libokularGenerator_magic.json")
|
|
|
|
MagicGenerator::MagicGenerator( QObject *parent, const QVariantList &args )
|
|
: Okular::Generator( parent, args )
|
|
{
|
|
}
|
|
|
|
MagicGenerator::~MagicGenerator()
|
|
{
|
|
}
|
|
|
|
bool MagicGenerator::loadDocument( const QString &fileName, QVector<Okular::Page*> &pages )
|
|
{
|
|
if ( !mMagicDocument.loadDocument( fileName ) ) {
|
|
emit error( i18n( "Unable to load document" ), -1 );
|
|
return false;
|
|
}
|
|
|
|
pagesVector.resize( mMagicDocument.numberOfPages() );
|
|
|
|
for ( int i = 0; i < mMagicDocument.numberOfPages(); ++i ) {
|
|
const QSize size = mMagicDocument.pageSize( i );
|
|
|
|
Okular::Page * page = new Okular::Page( i, size.width(), size.height(), Okular::Rotation0 );
|
|
pages[ i ] = page;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MagicGenerator::doCloseDocument()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool MagicGenerator::canGeneratePixmap() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void MagicGenerator::generatePixmap( Okular::PixmapRequest *request )
|
|
{
|
|
QImage image = mMagicDocument.pictureOfPage( request->pageNumber() );
|
|
|
|
image = image.scaled( request->width(), request->height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
|
|
|
|
request->page()->setPixmap( request->id(), new QPixmap( QPixmap::fromImage( image ) ) );
|
|
|
|
signalPixmapRequestDone( request );
|
|
}
|
|
|
|
\endcode
|
|
|
|
As you can see implementing a basic Generator is quite easy. The loadDocument() method opens the document file
|
|
and extracts the number of pages. For every page in the document it adds an Okular::Page object to the pages vector
|
|
which is passed in as method argument. Each page is initialized with its page number, width, height and initial rotation.
|
|
These page objects will be stored in the document object and act as a container for the picture representation
|
|
of the pages. This code is the same for nearly every Generator. On an failure the error() signal can be emitted
|
|
to inform the user about the issue. This code is the same for nearly every Generator.
|
|
|
|
In the doCloseDocument() method you should close the document and free all resources you have allocated in openDocument().
|
|
|
|
Now we come to the picture creation methods. The canGeneratorPixmap() method returns whether the Generator is currently
|
|
able to handle a new pixmap generation request. For a simple Generator like our one that's always the case as it works
|
|
linear, however a multithreaded Generator might return <em>false</em> here if it is still waiting for one of its working
|
|
threads to finish. In this case the document class will try to request the pixmap later again.
|
|
|
|
The generatePixmap() method does the actual fetching of the picture for a page. The page number, requested width and
|
|
height of the page is encapsulated in the passed Okular::PixmapRequest object.
|
|
So the task of the Generator is to create a pixmap of the requested page in the requested size and then store this
|
|
pixmap in the Okular::Page object which is associated with the page request.
|
|
When this task is finished, the Generator has to call signalPixmapRequestDone() with the page request object
|
|
as argument. This extra call is needed to allow the Generator to use signals and slots internally and create the
|
|
pixmap asynchronously.
|
|
|
|
So now you have the code of a working Okular Generator, the next step is to tell Okular about the new plugin.
|
|
Like in other places in KDE that is done by .desktop files, which are installed to the services directory.
|
|
|
|
Every Generator needs 1 .json, 3 .desktop files, and 1 .xml file:
|
|
|
|
\li libokularGenerator_<name>.json
|
|
\li okularApplication_<name>.desktop
|
|
\li okular<name>.desktop
|
|
\li org.kde.mobile.okular_<name>.desktop
|
|
\li org.kde.okular-<name>.metainfo.xml
|
|
|
|
where <name> should be the name of the document format. So for our Magic Document Generator we
|
|
create the following 4 files:
|
|
|
|
\li libokularGenerator_magic.json
|
|
\li okularApplication_magic.desktop
|
|
\li okularMagic.desktop
|
|
\li org.kde.mobile.okular_magic.desktop
|
|
\li org.kde.okular-magic.metainfo.xml
|
|
|
|
where libokularGenerator_magic.json has the following content something like this
|
|
|
|
\verbatim
|
|
{
|
|
"KPlugin": {
|
|
"Authors": [
|
|
{
|
|
"Email": "author@hosting.suffix",
|
|
"Name": "Proud Author",
|
|
}
|
|
],
|
|
"Copyright": "© 2042 Proud Author",
|
|
"Id": "okular_magic",
|
|
"License": "GPL",
|
|
"MimeTypes": [
|
|
"text/magic",
|
|
"text/x-magic"
|
|
],
|
|
"Name": "Magic Backend",
|
|
"ServiceTypes": [
|
|
"okular/Generator"
|
|
],
|
|
"Version": "0.1.0"
|
|
},
|
|
"X-KDE-Priority": 1,
|
|
"X-KDE-okularAPIVersion": 1,
|
|
"X-KDE-okularHasInternalSettings": true
|
|
}
|
|
\endverbatim
|
|
|
|
The last five fields has the special meaning to Okular
|
|
|
|
\li <b>ServiceType</b> Must be 'okular/Generator' for all Okular Generator Plugins
|
|
\li <b>MimeType</b> The mimetype or list of mimetypes of the supported document format(s)
|
|
\li <b>X-KDE-Priority</b> When multiple Generators for the same mimetype exists, the one with the highest priority is used
|
|
\li <b>X-KDE-okularAPIVersion</b> The version of the Generator Plugin API ('1' currently)
|
|
\li <b>X-KDE-okularHasInternalSettings</b> Is 'true' when the Generator provides configuration dialogs
|
|
|
|
The first .desktop file has the following content:
|
|
|
|
\verbatim
|
|
[Desktop Entry]
|
|
MimeType=application/x-magic;
|
|
Terminal=false
|
|
Name=okular
|
|
GenericName=Document Viewer
|
|
Exec=okular %U
|
|
Icon=okular
|
|
Type=Application
|
|
InitialPreference=7
|
|
Categories=Qt;KDE;Graphics;Viewer;
|
|
NoDisplay=true
|
|
X-KDE-Keywords=Magic
|
|
\endverbatim
|
|
|
|
You can use the file as it is, you just have to adapt the mimetype. This file is needed to allow Okular
|
|
to handle multiple mimetypes.
|
|
|
|
The second .desktop file looks like this:
|
|
|
|
\verbatim
|
|
[Desktop Entry]
|
|
Icon=okular
|
|
Name=okular
|
|
X-KDE-ServiceTypes=KParts/ReadOnlyPart
|
|
X-KDE-Library=okularpart
|
|
Type=Service
|
|
MimeType=application/x-magic;
|
|
\endverbatim
|
|
|
|
where
|
|
|
|
\li <b>X-KDE-Library</b> The name of the plugin library
|
|
|
|
You can use the file as it is as well, you just have to adapt the mimetype. This file is needed to allow
|
|
the Okular part to handle multiple mimetypes.
|
|
|
|
The third .desktop file contains data for the mobile version
|
|
|
|
\verbatim
|
|
[Desktop Entry]
|
|
MimeType=application/x-magic;
|
|
Name=Reader
|
|
GenericName=Document viewer
|
|
Comment=Viewer for various types of documents
|
|
TryExec=kpackagelauncherqml -a org.kde.mobile.okular
|
|
Exec=kpackagelauncherqml -a org.kde.mobile.okular %u
|
|
Terminal=false
|
|
Icon=okular
|
|
Type=Application
|
|
Categories=Qt;KDE;Graphics;Office;Viewer;
|
|
InitialPreference=2
|
|
NoDisplay=true
|
|
X-KDE-Keywords=Magic
|
|
\endverbatim
|
|
|
|
And the last .xml file has the following content
|
|
|
|
\verbatim
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<component type="addon">
|
|
<id>org.kde.okular-md</id>
|
|
<extends>org.kde.okular.desktop</extends>
|
|
<metadata_license>CC0-1.0</metadata_license>
|
|
<project_license>GPL-2.0+ and GFDL-1.3</project_license>
|
|
<name>Magic</name>
|
|
<summary>Adds support for reading Magic documents</summary>
|
|
<mimetypes>
|
|
<mimetype>application/magic</mimetype>
|
|
</mimetypes>
|
|
<url type="homepage">https://okular.kde.org</url>
|
|
</component>
|
|
\endverbatim
|
|
|
|
The last piece you need for a complete Generator is a CMakeLists.txt which compiles and installs the
|
|
Generator. Our CMakeLists.txt looks like the following:
|
|
|
|
\verbatim
|
|
add_definitions(-DTRANSLATION_DOMAIN="okular_magic")
|
|
|
|
macro_optional_find_package(Okular)
|
|
|
|
include_directories( ${OKULAR_INCLUDE_DIR} ${KF5_INCLUDE_DIR} ${QT_INCLUDES} )
|
|
|
|
########### next target ###############
|
|
|
|
set( okularGenerator_magic_PART_SRCS generator_magic.cpp )
|
|
|
|
target_link_libraries( okularGenerator_magic PRIVATE okularcore KF5::I18n KF5::KIOCore )
|
|
|
|
install( TARGETS okularGenerator_magic DESTINATION ${PLUGIN_INSTALL_DIR} )
|
|
|
|
########### install files ###############
|
|
|
|
install( FILES okularMagic.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} )
|
|
install( PROGRAMS okularApplication_magic.desktop org.kde.mobile.okular_magic.desktop DESTINATION ${KDE_INSTALL_APPDIR} )
|
|
install( FILES org.kde.okular-magic.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR} )
|
|
\endverbatim
|
|
|
|
The macro_optional_find_package(Okular) call is required to make the ${OKULAR_INCLUDE_DIR} and ${OKULAR_LIBRARIES}
|
|
variables available.
|
|
|
|
Now you can compile the Generator plugin and install it. After a restart of Okular the new plugin is available
|
|
and you can open Magic documents.
|
|
|
|
\section okular_generators_with_text A Generator with TextPage support
|
|
|
|
In this section we want to extend our Generator to support text search, text extraction and selection
|
|
as well. As mentioned in \ref okular_design_text_support, the Generator must provide an Okular::TextPage
|
|
object for every page which contains readable text.
|
|
|
|
Since we use the helper class MagicDocument to read the data from the document we have to extend it first,
|
|
so the new API looks as the following:
|
|
|
|
\code
|
|
class MagicDocument
|
|
{
|
|
public:
|
|
MagicDocument();
|
|
~MagicDocument();
|
|
|
|
bool loadDocument( const QString &fileName );
|
|
|
|
int numberOfPages() const;
|
|
|
|
QSize pageSize( int pageNumber ) const;
|
|
|
|
QImage pictureOfPage( int pageNumber ) const;
|
|
|
|
class TextInfo
|
|
{
|
|
public:
|
|
typedef QList<TextInfo> List;
|
|
|
|
QChar character;
|
|
qreal xPos;
|
|
qreal yPos;
|
|
qreal width;
|
|
qreal height;
|
|
};
|
|
|
|
TextInfo::List textOfPage( int pageNumber );
|
|
|
|
private:
|
|
...
|
|
};
|
|
\endcode
|
|
|
|
MagicDocument has the new internal class TextInfo now, which contains a character and
|
|
its absolute position on a page. Furthermore MagicDocument provides a method textOfPage()
|
|
which returns a list of all TextInfo objects for a page.
|
|
|
|
That's really an optimistic API, in reality it is sometimes quite hard to find out
|
|
the position of single characters in a document format.
|
|
|
|
With the extension of our helper class we can continue on extending our Generator now:
|
|
|
|
\code
|
|
#include "magicdocument.h"
|
|
|
|
#include <okular/core/generator.h>
|
|
|
|
class MagicGenerator : public Okular::Generator
|
|
{
|
|
public:
|
|
MagicGenerator( QObject *parent, const QVariantList &args );
|
|
~MagicGenerator();
|
|
|
|
bool loadDocument( const QString &fileName, QVector<Okular::Page*> &pages );
|
|
|
|
bool canGeneratePixmap() const;
|
|
void generatePixmap( Okular::PixmapRequest *request );
|
|
|
|
virtual bool canGenerateTextPage() const;
|
|
virtual void generateTextPage( Okular::Page *page, enum Okular::GenerationType type = Okular::Synchronous );
|
|
|
|
protected:
|
|
bool doCloseDocument();
|
|
|
|
private:
|
|
MagicDocument mMagicDocument;
|
|
};
|
|
\endcode
|
|
|
|
We have extended the MagicGenerator class by two methods canGenerateTextPage() and generateTextPage().
|
|
The first method is equal to canGeneratePixmap(), it returns whether the Generator is currently able to
|
|
handle a new text page generation request. For linear Generators that should be always the case, however
|
|
when the generation is done in a separated worker thread, this method might return <em>false</em>.
|
|
In this case the document class will try to request the text page later again.
|
|
|
|
The second method will generate the Okular::TextPage object for the passed page. Depending on the capabilities
|
|
of the Generator and the passed <em>type</em> parameter that is done synchronously or asynchronously.
|
|
|
|
Let us take a look at the implementation of these methods in our MagicGenerator:
|
|
|
|
\code
|
|
#include <okular/core/textpage.h>
|
|
|
|
...
|
|
|
|
MagicGenerator::MagicGenerator( QObject *parent, const QVariantList &args )
|
|
: Okular::Generator( parent, args )
|
|
{
|
|
setFeature( TextExtraction );
|
|
}
|
|
|
|
bool MagicGenerator::canGenerateTextPage() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void MagicGenerator::generateTextPage( Okular::Page *page, enum Okular::GenerationType )
|
|
{
|
|
MagicDocument::TextInfo::List characters = mMagicDocument.textOfPage( page->number() );
|
|
if ( characters.isEmpty() )
|
|
return;
|
|
|
|
Okular::TextPage *textPage = new Okular::TextPage;
|
|
for ( int i = 0; i < characters.count(); ++i ) {
|
|
qreal left = characters[ i ].xPos / page->width();
|
|
qreal top = characters[ i ].yPos / page->height();
|
|
qreal right = (characters[ i ].xPos + characters[ i ].width) / page->width();
|
|
qreal bottom = (characters[ i ].yPos + characters[ i ].height) / page->height();
|
|
|
|
textPage->append( characters[ i ].character,
|
|
new Okular::NormalizedRect( left, top, right, bottom ) );
|
|
}
|
|
|
|
page->setTextPage( textPage );
|
|
}
|
|
\endcode
|
|
|
|
As you can see the generateTextPage method just iterates over the list of characters returned
|
|
by our MagicDocument helper class and adds the character and its normalized bounding rect to
|
|
the Okular::TextPage object. At the end the text page is assigned to the page. We don't pay
|
|
attention to the GenerationType parameter here, if your Generator want to use threads, it should
|
|
check here whether the request shall be done asynchronously or synchronously and start the generation
|
|
according to that. Additionally we have to tell the Okular::Generator base class that we support
|
|
text handling by setting this flag in the constructor.
|
|
|
|
In this state we can now search, select and extract text from Magic documents.
|
|
|
|
\section okular_generators_threaded A Generator with Thread support
|
|
|
|
Sometimes it makes sense to do the generation of page pictures or text pages asynchronously to
|
|
improve performance and don't blocking the user interface. This can be done in two ways, either
|
|
by using signals and slots or by using threads. Both have there pros and cons:
|
|
|
|
<ul>
|
|
<li><b>Signals and Slots</b></li>
|
|
<ul>
|
|
<li>Pro: Can be used with backend libraries which are not thread safe</li>
|
|
<li>Con: Sometime difficult to implement</li>
|
|
</ul>
|
|
<li><b>Threads</b></li>
|
|
<ul>
|
|
<li>Pro: Easy to implement as you can make synchronous calls to the backend libraries</li>
|
|
<li>Con: Backend libraries must be thread safe and you must prevent race conditions by using mutexes</li>
|
|
</ul>
|
|
</ul>
|
|
|
|
The signal and slots approach can be achieved with a normal Generator by calling Okular::Generator::signalPixmapRequestDone()
|
|
from a slot after pixmap generation has been finished.
|
|
|
|
When using threads you should use a slightly different API, which hides most of the thread usage, to make
|
|
implementing as easy as possible.
|
|
|
|
Let's assume the pictureOfPage() and textOfPage methods in our MagicDocument helper class are thread safe,
|
|
so we can use them in a multithreaded environment.
|
|
So nothing prevents us from changing the MagicGenerator to use threads for better performance.
|
|
|
|
The new MagicGenerator API looks like the following:
|
|
|
|
\code
|
|
#include "magicdocument.h"
|
|
|
|
#include <okular/core/generator.h>
|
|
|
|
class MagicGenerator : public Okular::Generator
|
|
{
|
|
public:
|
|
MagicGenerator( QObject *parent, const QVariantList &args );
|
|
~MagicGenerator();
|
|
|
|
bool loadDocument( const QString &fileName, QVector<Okular::Page*> &pages );
|
|
|
|
protected:
|
|
bool doCloseDocument();
|
|
|
|
virtual QImage image( Okular::PixmapRequest *request );
|
|
|
|
virtual Okular::TextPage* textPage( Okular::Page *page );
|
|
|
|
private:
|
|
MagicDocument mMagicDocument;
|
|
};
|
|
\endcode
|
|
|
|
As you can see the canGeneratePixmap() generatePixmap(), canGenerateTextPage() and generateTextPage() methods have
|
|
been removed and replaced by the image() and textPage() methods.
|
|
|
|
Before explaining why, we'll take a look at the implementation:
|
|
|
|
\code
|
|
|
|
MagicGenerator::MagicGenerator( QObject *parent, const QVariantList &args )
|
|
: Okular::Generator( parent, args )
|
|
{
|
|
setFeature( TextExtraction );
|
|
setFeature( Threaded );
|
|
}
|
|
|
|
QImage MagicGenerator::image( Okular::PixmapRequest *request )
|
|
{
|
|
QImage image = mMagicDocument.pictureOfPage( request->pageNumber() );
|
|
|
|
return image.scaled( request->width(), request->height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
|
|
}
|
|
|
|
Okular::TextPage* textPage( Okular::Page *page )
|
|
{
|
|
MagicDocument::TextInfo::List characters = mMagicDocument.textOfPage( page->number() );
|
|
if ( characters.isEmpty() )
|
|
return 0;
|
|
|
|
Okular::TextPage *textPage = new Okular::TextPage;
|
|
for ( int i = 0; i < characters.count(); ++i ) {
|
|
qreal left = characters[ i ].xPos / page->width();
|
|
qreal top = characters[ i ].yPos / page->height();
|
|
qreal right = (characters[ i ].xPos + characters[ i ].width) / page->width();
|
|
qreal bottom = (characters[ i ].yPos + characters[ i ].height) / page->height();
|
|
|
|
textPage->append( characters[ i ].character,
|
|
new Okular::NormalizedRect( left, top, right, bottom ) );
|
|
}
|
|
|
|
return textPage;
|
|
}
|
|
\endcode
|
|
|
|
So the first obviously thing is that both methods return a value instead of modifying the page directly.
|
|
The reason for this is that both methods are executed in its own thread, so the code executed in them can
|
|
block as long as it wants, it won't block the GUI anyway. Additionally we have to tell the Okular::Generator
|
|
base class that we can handle threads by setting the flag in the constructor.
|
|
|
|
With only a small change we made our MagicGenerator multithreaded now!
|
|
|
|
\section okular_generators_extended An Extended Generator
|
|
|
|
Now we want to create a new generator with some additional functionality:
|
|
|
|
\li Support for document information (author, creation date etc.)
|
|
\li Support for a table of content
|
|
\li Support for printing the document
|
|
\li Support for exporting the document as text
|
|
|
|
The new Generator shall be able to handle HTML documents. We choose this format as example, because
|
|
we can use QTextDocument to load, render and print a HTML page, so a lot of code can be reused.
|
|
|
|
The API of our HTMLGenerator looks like the following:
|
|
|
|
\code
|
|
#include <QtGui/QTextDocument>
|
|
|
|
#include <okular/core/generator.h>
|
|
|
|
class HTMLGenerator : public Okular::Generator
|
|
{
|
|
public:
|
|
HTMLGenerator( QObject *parent, const QVariantList &args );
|
|
~HTMLGenerator();
|
|
|
|
bool loadDocument( const QString &fileName, QVector<Okular::Page*> &pages );
|
|
|
|
bool canGeneratePixmap() const;
|
|
void generatePixmap( Okular::PixmapRequest *request );
|
|
|
|
virtual Okular::DocumentInfo generateDocumentInfo( const QSet<Okular::DocumentInfo::Key> &keys ) const;
|
|
|
|
virtual const Okular::DocumentSynopsis* generateDocumentSynopsis();
|
|
|
|
virtual bool print( KPrinter &printer );
|
|
|
|
virtual Okular::ExportFormat::List exportFormats() const;
|
|
|
|
virtual bool exportTo( const QString &fileName, const Okular::ExportFormat &format );
|
|
|
|
protected:
|
|
bool doCloseDocument();
|
|
|
|
private:
|
|
QTextDocument *mTextDocument;
|
|
Okular::DocumentInfo mDocumentInfo;
|
|
Okular::DocumentSynopsis mDocumentSynopsis;
|
|
};
|
|
\endcode
|
|
|
|
The Generator doesn't support text search and selection, as the code would be quite complex, we'll show
|
|
how to do it in the next chapter (not yet written) anyway.
|
|
|
|
As you can see we have 5 new methods in the class:
|
|
|
|
\li <b>generateDocumentInfo()</b> Creates an Okular::DocumentInfo (which is in fact a QDomDocument)
|
|
which contains document information like author, creation time etc.
|
|
\li <b>generateDocumentSynopsis()</b> Creates an Okular::DocumentSynopsis (which is a QDomDocument as well)
|
|
which contains the table of content.
|
|
\li <b>print()</b> Prints the document to the passed printer.
|
|
\li <b>exportFormats()</b> Returns the supported export formats.
|
|
\li <b>exportTo()</b> Exports the document to the given file in the given format.
|
|
|
|
Now that you know what the methods are supposed to do, let's take a look at the implementation:
|
|
|
|
\code
|
|
#include <QFile>
|
|
#include <QAbstractTextDocumentLayout>
|
|
#include <QPrinter>
|
|
|
|
#include <okular/core/document.h>
|
|
#include <okular/core/page.h>
|
|
|
|
#include "htmlgenerator.h"
|
|
|
|
#include <KLocalizedString>
|
|
|
|
OKULAR_EXPORT_PLUGIN(HTMLGenerator, "libokularGenerator_html.json")
|
|
|
|
HTMLGenerator::HTMLGenerator( QObject *parent, const QVariantList &args )
|
|
: Okular::Generator( parent, args ),
|
|
mTextDocument( 0 )
|
|
{
|
|
}
|
|
|
|
HTMLGenerator::~HTMLGenerator()
|
|
{
|
|
delete mTextDocument;
|
|
}
|
|
|
|
bool HTMLGenerator::loadDocument( const QString &fileName, QVector<Okular::Page*> &pages )
|
|
{
|
|
QFile file( fileName );
|
|
if ( !file.open( QIODevice::ReadOnly ) ) {
|
|
emit error( i18n( "Unable to open file" ), -1 );
|
|
return false;
|
|
}
|
|
|
|
const QString data = QString::fromUtf8( file.readAll() );
|
|
|
|
file.close();
|
|
|
|
mTextDocument = new QTextDocument;
|
|
mTextDocument->setHtml( data );
|
|
mTextDocument->setPageSize( QSizeF( 600, 800 ) );
|
|
|
|
pages.resize( mTextDocument->pageCount() );
|
|
|
|
for ( int i = 0; i < mTextDocument->pageCount(); ++i ) {
|
|
Okular::Page * page = new Okular::Page( i, 600, 800, Okular::Rotation0 );
|
|
pages[ i ] = page;
|
|
}
|
|
|
|
mDocumentInfo.set( "author", "Tobias Koenig", i18n( "Author" ) );
|
|
mDocumentInfo.set( "title", "The Art of Okular Plugin Development", i18n( "Title" ) );
|
|
|
|
Okular::DocumentViewport viewport = ... // get the viewport of the chapter
|
|
|
|
QDomElement item = mDocumentSynopsis.createElement( "Chapter 1" );
|
|
item.setAttribute( "Viewport", viewport.toString() );
|
|
mDocumentSynopsis.appendChild( item );
|
|
|
|
viewport = ... // get the viewport of the subchapter
|
|
|
|
QDomElement childItem = mDocumentSynopsis.createElement( "SubChapter 1.1" );
|
|
childItem.setAttribute( "Viewport", viewport.toString() );
|
|
item.appendChild( childItem );
|
|
|
|
return true;
|
|
}
|
|
|
|
bool HTMLGenerator::doCloseDocument()
|
|
{
|
|
delete mTextDocument;
|
|
mTextDocument = 0;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool HTMLGenerator::canGeneratePixmap() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void HTMLGenerator::generatePixmap( Okular::PixmapRequest *request )
|
|
{
|
|
QPixmap *pixmap = new QPixmap( request->width(), request->height() );
|
|
pixmap->fill( Qt::white );
|
|
|
|
QPainter p;
|
|
p.begin( pixmap );
|
|
|
|
qreal width = request->width();
|
|
qreal height = request->height();
|
|
|
|
p.scale( width / 600, height / 800 );
|
|
|
|
const QRect rect( 0, request->pageNumber() * 800, 600, 800 );
|
|
p.translate( QPoint( 0, request->pageNumber() * -800 ) );
|
|
d->mDocument->drawContents( &p, rect );
|
|
p.end();
|
|
|
|
request->page()->setPixmap( request->id(), pixmap );
|
|
|
|
signalPixmapRequestDone( request );
|
|
}
|
|
|
|
Okular::DocumentInfo HTMLGenerator::generateDocumentInfo( const QSet<Okular::DocumentInfo::Key> &keys ) const
|
|
{
|
|
return mDocumentInfo;
|
|
}
|
|
|
|
const Okular::DocumentSynopsis* HTMLGenerator::generateDocumentSynopsis()
|
|
{
|
|
if ( !mDocumentSynopsis.hasChildNodes() )
|
|
return 0;
|
|
else
|
|
return &mDocumentSynopsis;
|
|
}
|
|
|
|
bool HTMLGenerator::print( KPrinter &printer )
|
|
{
|
|
QPainter p( &printer );
|
|
|
|
for ( int i = 0; i < mTextDocument->pageCount(); ++i ) {
|
|
if ( i != 0 )
|
|
printer.newPage();
|
|
|
|
QRect rect( 0, i * 800, 600, 800 );
|
|
p.translate( QPoint( 0, i * -800 ) );
|
|
mTextDocument->drawContents( &p, rect );
|
|
}
|
|
}
|
|
|
|
Okular::ExportFormat::List HTMLGenerator::exportFormats() const
|
|
{
|
|
return Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText );
|
|
}
|
|
|
|
bool HTMLGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format )
|
|
{
|
|
QFile file( fileName );
|
|
if ( !fileName.open( QIODevice::WriteOnly ) ) {
|
|
emit error( i18n( "Unable to open file" ), -1 );
|
|
return false;
|
|
}
|
|
|
|
if ( format.mimeType()->name() == QLatin1String( "text/plain" ) )
|
|
file.writeBlock( mTextDocument->toPlainText().toUtf8() );
|
|
|
|
file.close();
|
|
|
|
return true;
|
|
}
|
|
\endcode
|
|
|
|
Let's take a closer look at the single methods. In the loadDocument() method we try to open the
|
|
passed file name and read all the content into the QTextDocument object. By calling
|
|
QTextDocument::setPageSize(), the whole document is divided into pages of the given size.
|
|
In the next step we create Okular::Page objects for every page in the QTextDocument and fill
|
|
the pages vector with them.
|
|
|
|
Afterwards we fill our Okular::DocumentInfo object with data. Since extracting the HTML meta data
|
|
would need a lot of code we work with static data here. [to be continued]
|
|
*/
|