mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start work on a headless QPA plugin to allow Qt to be used without an X server in linux.
Useful for the various command line tools that require Qt. For example conversion with PDF Output or MOBI output + rasterization of SVG images. Or ebook-meta with rendering of first page of EPUB as cover. Or downloading news using a WebKit browser as the backend. Makes it unnecessary to use xvfb and will hopefully end the pointless bug reports from people that try to use calibre command line tools in environments without X.
This commit is contained in:
parent
dad004e03c
commit
8da98e8ba4
@ -25,7 +25,8 @@ py_lib_dir = os.path.join(sys.prefix, 'lib')
|
|||||||
|
|
||||||
class Extension(object):
|
class Extension(object):
|
||||||
|
|
||||||
def absolutize(self, paths):
|
@classmethod
|
||||||
|
def absolutize(cls, paths):
|
||||||
return list(set([x if os.path.isabs(x) else os.path.join(SRC, x.replace('/',
|
return list(set([x if os.path.isabs(x) else os.path.join(SRC, x.replace('/',
|
||||||
os.sep)) for x in paths]))
|
os.sep)) for x in paths]))
|
||||||
|
|
||||||
@ -391,7 +392,7 @@ class Build(Command):
|
|||||||
''')
|
''')
|
||||||
|
|
||||||
def add_options(self, parser):
|
def add_options(self, parser):
|
||||||
choices = [e.name for e in extensions]+['all', 'style']
|
choices = [e.name for e in extensions]+['all', 'headless']
|
||||||
parser.add_option('-1', '--only', choices=choices, default='all',
|
parser.add_option('-1', '--only', choices=choices, default='all',
|
||||||
help=('Build only the named extension. Available: '+
|
help=('Build only the named extension. Available: '+
|
||||||
', '.join(choices)+'. Default:%default'))
|
', '.join(choices)+'. Default:%default'))
|
||||||
@ -419,10 +420,12 @@ class Build(Command):
|
|||||||
os.makedirs(self.d(dest))
|
os.makedirs(self.d(dest))
|
||||||
self.info('\n####### Building extension', ext.name, '#'*7)
|
self.info('\n####### Building extension', ext.name, '#'*7)
|
||||||
self.build(ext, dest)
|
self.build(ext, dest)
|
||||||
|
if opts.only in {'all', 'headless'}:
|
||||||
|
self.build_headless()
|
||||||
|
|
||||||
def dest(self, ext):
|
def dest(self, ext):
|
||||||
ex = '.pyd' if iswindows else '.so'
|
ex = '.pyd' if iswindows else '.so'
|
||||||
return os.path.join(SRC, 'calibre', 'plugins', ext.name)+ex
|
return os.path.join(SRC, 'calibre', 'plugins', getattr(ext, 'name', ext))+ex
|
||||||
|
|
||||||
def inc_dirs_to_cflags(self, dirs):
|
def inc_dirs_to_cflags(self, dirs):
|
||||||
return ['-I'+x for x in dirs]
|
return ['-I'+x for x in dirs]
|
||||||
@ -500,6 +503,46 @@ class Build(Command):
|
|||||||
print "Error while executing: %s\n" % (cmdline)
|
print "Error while executing: %s\n" % (cmdline)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def build_headless(self):
|
||||||
|
if iswindows or isosx:
|
||||||
|
return # Dont have headless operation on these platforms
|
||||||
|
self.info('\n####### Building headless QPA plugin', '#'*7)
|
||||||
|
a = Extension.absolutize
|
||||||
|
headers = a(['calibre/headless/headless_backingstore.h', 'calibre/headless/headless_integration.h'])
|
||||||
|
sources = a(['calibre/headless/main.cpp', 'calibre/headless/headless_backingstore.cpp', 'calibre/headless/headless_integration.cpp'])
|
||||||
|
others = a(['calibre/headless/headless.json'])
|
||||||
|
target = self.dest('headless')
|
||||||
|
if not self.newer(target, headers + sources + others):
|
||||||
|
return
|
||||||
|
pro = textwrap.dedent(
|
||||||
|
'''\
|
||||||
|
TARGET = headless
|
||||||
|
PLUGIN_TYPE = platforms
|
||||||
|
PLUGIN_CLASS_NAME = HeadlessIntegrationPlugin
|
||||||
|
load(qt_plugin)
|
||||||
|
QT += core-private gui-private platformsupport-private
|
||||||
|
HEADERS = {headers}
|
||||||
|
SOURCES = {sources}
|
||||||
|
OTHER_FILES = {others}
|
||||||
|
DESTDIR = {destdir}
|
||||||
|
CONFIG -= create_cmake # Prevent qmake from generating a cmake build file which it puts in the calibre src directory
|
||||||
|
''').format(
|
||||||
|
headers=' '.join(headers), sources=' '.join(sources), others=' '.join(others), destdir=self.d(target))
|
||||||
|
bdir = self.j(self.d(self.SRC), 'build', 'headless')
|
||||||
|
if not os.path.exists(bdir):
|
||||||
|
os.makedirs(bdir)
|
||||||
|
pf = self.j(bdir, 'headless.pro')
|
||||||
|
open(self.j(bdir, '.qmake.conf'), 'wb').close()
|
||||||
|
with open(pf, 'wb') as f:
|
||||||
|
f.write(pro.encode('utf-8'))
|
||||||
|
cwd = os.getcwd()
|
||||||
|
os.chdir(bdir)
|
||||||
|
try:
|
||||||
|
self.check_call([QMAKE] + [self.b(pf)])
|
||||||
|
self.check_call([make] + ['-j%d'%(cpu_count() or 1)])
|
||||||
|
finally:
|
||||||
|
os.chdir(cwd)
|
||||||
|
|
||||||
def build_sip_files(self, ext, src_dir):
|
def build_sip_files(self, ext, src_dir):
|
||||||
sip_files = ext.sip_files
|
sip_files = ext.sip_files
|
||||||
ext.sip_files = []
|
ext.sip_files = []
|
||||||
|
@ -875,11 +875,14 @@ def detach_gui():
|
|||||||
|
|
||||||
class Application(QApplication):
|
class Application(QApplication):
|
||||||
|
|
||||||
def __init__(self, args, force_calibre_style=False,
|
def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False):
|
||||||
override_program_name=None):
|
|
||||||
self.file_event_hook = None
|
self.file_event_hook = None
|
||||||
if override_program_name:
|
if override_program_name:
|
||||||
args = [override_program_name] + args[1:]
|
args = [override_program_name] + args[1:]
|
||||||
|
if headless:
|
||||||
|
if not args:
|
||||||
|
args = sys.argv[:1]
|
||||||
|
args.extend(['-platformpluginpath', sys.extensions_location, '-platform', 'headless'])
|
||||||
qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
|
qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args]
|
||||||
self.pi = plugins['progress_indicator'][0]
|
self.pi = plugins['progress_indicator'][0]
|
||||||
self.setup_styles(force_calibre_style)
|
self.setup_styles(force_calibre_style)
|
||||||
|
0
src/calibre/headless/.qmake.conf
Normal file
0
src/calibre/headless/.qmake.conf
Normal file
3
src/calibre/headless/headless.json
Normal file
3
src/calibre/headless/headless.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"Keys": [ "headless" ]
|
||||||
|
}
|
51
src/calibre/headless/headless_backingstore.cpp
Normal file
51
src/calibre/headless/headless_backingstore.cpp
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#include "headless_backingstore.h"
|
||||||
|
#include "headless_integration.h"
|
||||||
|
#include "qscreen.h"
|
||||||
|
#include <QtCore/qdebug.h>
|
||||||
|
#include <qpa/qplatformscreen.h>
|
||||||
|
#include <private/qguiapplication_p.h>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
|
HeadlessBackingStore::HeadlessBackingStore(QWindow *window)
|
||||||
|
: QPlatformBackingStore(window)
|
||||||
|
, mDebug(HeadlessIntegration::instance()->options() & HeadlessIntegration::DebugBackingStore)
|
||||||
|
{
|
||||||
|
if (mDebug)
|
||||||
|
qDebug() << "HeadlessBackingStore::HeadlessBackingStore:" << (quintptr)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessBackingStore::~HeadlessBackingStore()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QPaintDevice *HeadlessBackingStore::paintDevice()
|
||||||
|
{
|
||||||
|
if (mDebug)
|
||||||
|
qDebug() << "HeadlessBackingStore::paintDevice";
|
||||||
|
|
||||||
|
return &mImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadlessBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset)
|
||||||
|
{
|
||||||
|
Q_UNUSED(window);
|
||||||
|
Q_UNUSED(region);
|
||||||
|
Q_UNUSED(offset);
|
||||||
|
|
||||||
|
if (mDebug) {
|
||||||
|
static int c = 0;
|
||||||
|
QString filename = QString("output%1.png").arg(c++, 4, 10, QLatin1Char('0'));
|
||||||
|
qDebug() << "HeadlessBackingStore::flush() saving contents to" << filename.toLocal8Bit().constData();
|
||||||
|
mImage.save(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeadlessBackingStore::resize(const QSize &size, const QRegion &)
|
||||||
|
{
|
||||||
|
QImage::Format format = QGuiApplication::primaryScreen()->handle()->format();
|
||||||
|
if (mImage.size() != size)
|
||||||
|
mImage = QImage(size, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
QT_END_NAMESPACE
|
25
src/calibre/headless/headless_backingstore.h
Normal file
25
src/calibre/headless/headless_backingstore.h
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qpa/qplatformbackingstore.h>
|
||||||
|
#include <qpa/qplatformwindow.h>
|
||||||
|
#include <QtGui/QImage>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
|
class HeadlessBackingStore : public QPlatformBackingStore
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HeadlessBackingStore(QWindow *window);
|
||||||
|
~HeadlessBackingStore();
|
||||||
|
|
||||||
|
QPaintDevice *paintDevice();
|
||||||
|
void flush(QWindow *window, const QRegion ®ion, const QPoint &offset);
|
||||||
|
void resize(const QSize &size, const QRegion &staticContents);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QImage mImage;
|
||||||
|
const bool mDebug;
|
||||||
|
};
|
||||||
|
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
105
src/calibre/headless/headless_integration.cpp
Normal file
105
src/calibre/headless/headless_integration.cpp
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#include "headless_integration.h"
|
||||||
|
#include "headless_backingstore.h"
|
||||||
|
#ifndef Q_OS_WIN
|
||||||
|
#include <QtPlatformSupport/private/qgenericunixeventdispatcher_p.h>
|
||||||
|
#else
|
||||||
|
#include <QtCore/private/qeventdispatcher_win_p.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <QtGui/private/qpixmap_raster_p.h>
|
||||||
|
#include <QtGui/private/qguiapplication_p.h>
|
||||||
|
#include <qpa/qplatformwindow.h>
|
||||||
|
#include <qpa/qplatformfontdatabase.h>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
|
static const char debugBackingStoreEnvironmentVariable[] = "QT_DEBUG_BACKINGSTORE";
|
||||||
|
|
||||||
|
static inline unsigned parseOptions(const QStringList ¶mList)
|
||||||
|
{
|
||||||
|
unsigned options = 0;
|
||||||
|
foreach (const QString ¶m, paramList) {
|
||||||
|
if (param == QLatin1String("enable_fonts"))
|
||||||
|
options |= HeadlessIntegration::EnableFonts;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessIntegration::HeadlessIntegration(const QStringList ¶meters)
|
||||||
|
: m_dummyFontDatabase(0)
|
||||||
|
, m_options(parseOptions(parameters))
|
||||||
|
{
|
||||||
|
if (qEnvironmentVariableIsSet(debugBackingStoreEnvironmentVariable)
|
||||||
|
&& qgetenv(debugBackingStoreEnvironmentVariable).toInt() > 0) {
|
||||||
|
m_options |= DebugBackingStore | EnableFonts;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessScreen *mPrimaryScreen = new HeadlessScreen();
|
||||||
|
|
||||||
|
mPrimaryScreen->mGeometry = QRect(0, 0, 240, 320);
|
||||||
|
mPrimaryScreen->mDepth = 32;
|
||||||
|
mPrimaryScreen->mFormat = QImage::Format_ARGB32_Premultiplied;
|
||||||
|
|
||||||
|
screenAdded(mPrimaryScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessIntegration::~HeadlessIntegration()
|
||||||
|
{
|
||||||
|
delete m_dummyFontDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HeadlessIntegration::hasCapability(QPlatformIntegration::Capability cap) const
|
||||||
|
{
|
||||||
|
switch (cap) {
|
||||||
|
case ThreadedPixmaps: return true;
|
||||||
|
case MultipleWindows: return true;
|
||||||
|
default: return QPlatformIntegration::hasCapability(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dummy font database that does not scan the fonts directory to be
|
||||||
|
// used for command line tools like qmlplugindump that do not create windows
|
||||||
|
// unless DebugBackingStore is activated.
|
||||||
|
class DummyFontDatabase : public QPlatformFontDatabase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void populateFontDatabase() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
QPlatformFontDatabase *HeadlessIntegration::fontDatabase() const
|
||||||
|
{
|
||||||
|
if (m_options & EnableFonts)
|
||||||
|
return QPlatformIntegration::fontDatabase();
|
||||||
|
if (!m_dummyFontDatabase)
|
||||||
|
m_dummyFontDatabase = new DummyFontDatabase;
|
||||||
|
return m_dummyFontDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPlatformWindow *HeadlessIntegration::createPlatformWindow(QWindow *window) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(window);
|
||||||
|
QPlatformWindow *w = new QPlatformWindow(window);
|
||||||
|
w->requestActivateWindow();
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPlatformBackingStore *HeadlessIntegration::createPlatformBackingStore(QWindow *window) const
|
||||||
|
{
|
||||||
|
return new HeadlessBackingStore(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
QAbstractEventDispatcher *HeadlessIntegration::createEventDispatcher() const
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
return new QEventDispatcherWin32;
|
||||||
|
#else
|
||||||
|
return createUnixEventDispatcher();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessIntegration *HeadlessIntegration::instance()
|
||||||
|
{
|
||||||
|
return static_cast<HeadlessIntegration *>(QGuiApplicationPrivate::platformIntegration());
|
||||||
|
}
|
||||||
|
|
||||||
|
QT_END_NAMESPACE
|
53
src/calibre/headless/headless_integration.h
Normal file
53
src/calibre/headless/headless_integration.h
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qpa/qplatformintegration.h>
|
||||||
|
#include <qpa/qplatformscreen.h>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
|
class HeadlessScreen : public QPlatformScreen
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HeadlessScreen()
|
||||||
|
: mDepth(32), mFormat(QImage::Format_ARGB32_Premultiplied) {}
|
||||||
|
|
||||||
|
QRect geometry() const { return mGeometry; }
|
||||||
|
int depth() const { return mDepth; }
|
||||||
|
QImage::Format format() const { return mFormat; }
|
||||||
|
|
||||||
|
public:
|
||||||
|
QRect mGeometry;
|
||||||
|
int mDepth;
|
||||||
|
QImage::Format mFormat;
|
||||||
|
QSize mPhysicalSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
class HeadlessIntegration : public QPlatformIntegration
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Options { // Options to be passed on command line or determined from environment
|
||||||
|
DebugBackingStore = 0x1,
|
||||||
|
EnableFonts = 0x2
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit HeadlessIntegration(const QStringList ¶meters);
|
||||||
|
~HeadlessIntegration();
|
||||||
|
|
||||||
|
bool hasCapability(QPlatformIntegration::Capability cap) const;
|
||||||
|
QPlatformFontDatabase *fontDatabase() const;
|
||||||
|
|
||||||
|
QPlatformWindow *createPlatformWindow(QWindow *window) const;
|
||||||
|
QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const;
|
||||||
|
QAbstractEventDispatcher *createEventDispatcher() const;
|
||||||
|
|
||||||
|
unsigned options() const { return m_options; }
|
||||||
|
|
||||||
|
static HeadlessIntegration *instance();
|
||||||
|
|
||||||
|
private:
|
||||||
|
mutable QPlatformFontDatabase *m_dummyFontDatabase;
|
||||||
|
unsigned m_options;
|
||||||
|
};
|
||||||
|
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
24
src/calibre/headless/main.cpp
Normal file
24
src/calibre/headless/main.cpp
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#include <qpa/qplatformintegrationplugin.h>
|
||||||
|
#include "headless_integration.h"
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
|
class HeadlessIntegrationPlugin : public QPlatformIntegrationPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QPA.QPlatformIntegrationFactoryInterface.5.2" FILE "headless.json")
|
||||||
|
public:
|
||||||
|
QPlatformIntegration *create(const QString&, const QStringList&);
|
||||||
|
};
|
||||||
|
|
||||||
|
QPlatformIntegration *HeadlessIntegrationPlugin::create(const QString& system, const QStringList& paramList)
|
||||||
|
{
|
||||||
|
if (!system.compare(QLatin1String("headless"), Qt::CaseInsensitive))
|
||||||
|
return new HeadlessIntegration(paramList);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
#include "main.moc"
|
@ -12,7 +12,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
Test a binary calibre build to ensure that all needed binary images/libraries have loaded.
|
Test a binary calibre build to ensure that all needed binary images/libraries have loaded.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import cStringIO
|
import cStringIO, os
|
||||||
from calibre.constants import plugins, iswindows, islinux
|
from calibre.constants import plugins, iswindows, islinux
|
||||||
|
|
||||||
def test_dbus():
|
def test_dbus():
|
||||||
@ -76,17 +76,23 @@ def test_apsw():
|
|||||||
print ('apsw OK!')
|
print ('apsw OK!')
|
||||||
|
|
||||||
def test_qt():
|
def test_qt():
|
||||||
from PyQt5.Qt import (QDialog, QImageReader, QNetworkAccessManager)
|
from calibre.gui2 import Application
|
||||||
|
from PyQt5.Qt import (QImageReader, QNetworkAccessManager)
|
||||||
from PyQt5.QtWebKitWidgets import QWebView
|
from PyQt5.QtWebKitWidgets import QWebView
|
||||||
|
os.environ.pop('DISPLAY', None)
|
||||||
|
app = Application([], headless=islinux)
|
||||||
fmts = set(map(unicode, QImageReader.supportedImageFormats()))
|
fmts = set(map(unicode, QImageReader.supportedImageFormats()))
|
||||||
testf = set(['jpg', 'png', 'mng', 'svg', 'ico', 'gif'])
|
testf = set(['jpg', 'png', 'mng', 'svg', 'ico', 'gif'])
|
||||||
if testf.intersection(fmts) != testf:
|
if testf.intersection(fmts) != testf:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Qt doesn't seem to be able to load its image plugins")
|
"Qt doesn't seem to be able to load its image plugins")
|
||||||
QWebView, QDialog
|
QWebView()
|
||||||
|
del QWebView
|
||||||
na = QNetworkAccessManager()
|
na = QNetworkAccessManager()
|
||||||
if not hasattr(na, 'sslErrors'):
|
if not hasattr(na, 'sslErrors'):
|
||||||
raise RuntimeError('Qt not compiled with openssl')
|
raise RuntimeError('Qt not compiled with openssl')
|
||||||
|
del na
|
||||||
|
del app
|
||||||
print ('Qt OK!')
|
print ('Qt OK!')
|
||||||
|
|
||||||
def test_imaging():
|
def test_imaging():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user