mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
f386fcb539
@ -555,7 +555,7 @@ There can be two reasons why |app| is showing a empty list of books:
|
||||
|
||||
* Your |app| library folder changed its location. This can happen if it was on an external disk and the drive letter for that disk changed. Or if you accidentally moved the folder. In this case, |app| cannot find its library and so starts up with an empty library instead. To remedy this, do a right-click on the |app| icon in the |app| toolbar (it will say 0 books underneath it) and select Switch/create library. Click the little blue icon to select the new location of your |app| library and click OK.
|
||||
|
||||
* Your metadata.db file was deleted/corrupted. In this case, you can ask |app| to rebuild the metadata.db from its backups. Click-and-hold the |app| icon in the |app| toolbar (it will say 0 books underneath it) and select Library maintenance->Restore database. |app| will automatically rebuild metadata.db.
|
||||
* Your metadata.db file was deleted/corrupted. In this case, you can ask |app| to rebuild the metadata.db from its backups. Right click the |app| icon in the |app| toolbar (it will say 0 books underneath it) and select Library maintenance->Restore database. |app| will automatically rebuild metadata.db.
|
||||
|
||||
|
||||
Content From The Web
|
||||
|
@ -6,40 +6,19 @@ www.foreignpolicy.com
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class ForeignPolicy(BasicNewsRecipe):
|
||||
title = 'Foreign Policy'
|
||||
class AdvancedUserRecipe1349086293(BasicNewsRecipe):
|
||||
title = u'Foreign Policy'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'International News'
|
||||
publisher = 'Washingtonpost.Newsweek Interactive, LLC'
|
||||
category = 'news, politics, USA'
|
||||
oldest_article = 31
|
||||
oldest_article = 31
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'utf8'
|
||||
use_embedded_content = False
|
||||
language = 'en'
|
||||
remove_empty_feeds = True
|
||||
extra_css = ' body{font-family: Georgia,"Times New Roman",Times,serif } img{margin-bottom: 0.4em} h1,h2,h3,h4,h5,h6{font-family: Arial,Helvetica,sans-serif} '
|
||||
auto_cleanup = True
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
keep_only_tags = [dict(attrs={'id':['art-mast','art-body','auth-bio']})]
|
||||
remove_tags = [dict(name='iframe'),dict(attrs={'id':['share-box','base-ad']})]
|
||||
remove_attributes = ['height','width']
|
||||
|
||||
|
||||
feeds = [(u'Articles', u'http://www.foreignpolicy.com/node/feed')]
|
||||
feeds = [(u'Foreign_Policy', u'http://www.foreignpolicy.com/node/feed')]
|
||||
|
||||
def print_version(self, url):
|
||||
return url + '?print=yes&page=full'
|
||||
return url + '?print=yes&hidecomments=yes&page=full'
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return soup
|
||||
|
||||
|
@ -11,23 +11,8 @@ class NatureNews(BasicNewsRecipe):
|
||||
max_articles_per_feed = 50
|
||||
|
||||
no_stylesheets = True
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'content'})]
|
||||
# remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'})
|
||||
# remove_tags_after = dict(name='h2', attrs={'id':'comments'})
|
||||
remove_tags = [
|
||||
dict(name='h2', attrs={'id':'comments'}),
|
||||
dict(attrs={'alt':'Advertisement'}),
|
||||
dict(name='div', attrs={'class':'ad'}),
|
||||
dict(attrs={'class':'Z3988'}),
|
||||
dict(attrs={'class':['formatpublished','type-of-article','cleardiv','disclaimer','buttons','comments xoxo']}),
|
||||
dict(name='a', attrs={'href':'#comments'}),
|
||||
dict(name='h2',attrs={'class':'subheading plusicon icon-add-comment'})
|
||||
]
|
||||
|
||||
preprocess_regexps = [
|
||||
(re.compile(r'<p>ADVERTISEMENT</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
|
||||
]
|
||||
|
||||
use_embedded_content = False
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'article'})]
|
||||
extra_css = '''
|
||||
.author { text-align: right; font-size: small; line-height:1em; margin-top:0px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||
.imagedescription { font-size: small; font-style:italic; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
|
||||
@ -36,51 +21,3 @@ class NatureNews(BasicNewsRecipe):
|
||||
|
||||
feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')]
|
||||
|
||||
def preprocess_html(self,soup):
|
||||
# The author name is slightly buried - dig it up
|
||||
author = soup.find('p', {'class':'byline'})
|
||||
if author:
|
||||
# Find out the author's name
|
||||
authornamediv = author.find('span',{'class':'author fn'})
|
||||
authornamelink = authornamediv.find('a')
|
||||
if authornamelink:
|
||||
authorname = authornamelink.contents[0]
|
||||
else:
|
||||
authorname = authornamediv.contents[0]
|
||||
# Stick the author's name in the byline tag
|
||||
tag = Tag(soup,'div')
|
||||
tag['class'] = 'author'
|
||||
tag.insert(0,authorname.strip())
|
||||
author.replaceWith(tag)
|
||||
|
||||
# Change the intro from a p to a div
|
||||
intro = soup.find('p',{'class':'intro'})
|
||||
if intro:
|
||||
tag = Tag(soup,'div')
|
||||
tag['class'] = 'intro'
|
||||
tag.insert(0,intro.contents[0])
|
||||
intro.replaceWith(tag)
|
||||
|
||||
# Change span class=imagedescription to div
|
||||
descr = soup.find('span',{'class':'imagedescription'})
|
||||
if descr:
|
||||
tag = Tag(soup,'div')
|
||||
tag['class'] = 'imagedescription'
|
||||
tag.insert(0,descr.renderContents())
|
||||
descr.replaceWith(tag)
|
||||
|
||||
# The references are in a list, let's make them simpler
|
||||
reflistcont = soup.find('ul',{'id':'article-refrences'})
|
||||
if reflistcont:
|
||||
reflist = reflistcont.li.renderContents()
|
||||
tag = Tag(soup,'div')
|
||||
tag['class'] = 'article-references'
|
||||
tag.insert(0,reflist)
|
||||
reflistcont.replaceWith(tag)
|
||||
|
||||
# Within the id=content div, we need to remove all the stuff after the end of the class=entry-content
|
||||
entrycontent = soup.find('div',{'class':'entry-content'})
|
||||
for nextSibling in entrycontent.findNextSiblings():
|
||||
nextSibling.extract()
|
||||
|
||||
return soup
|
||||
|
BIN
recipes/icons/automatiseringgids.png
Normal file
BIN
recipes/icons/automatiseringgids.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
recipes/icons/fokkeensukke.png
Normal file
BIN
recipes/icons/fokkeensukke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
BIN
recipes/icons/tweakers_net.png
Normal file
BIN
recipes/icons/tweakers_net.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
recipes/icons/vrijnederland.png
Normal file
BIN
recipes/icons/vrijnederland.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
@ -1,5 +1,5 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2009-2012, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
twitchfilm.net/news/
|
||||
'''
|
||||
@ -15,7 +15,6 @@ class Twitchfilm(BasicNewsRecipe):
|
||||
use_embedded_content = False
|
||||
encoding = 'utf-8'
|
||||
publisher = 'Twitch'
|
||||
masthead_url = 'http://twitchfilm.com/img/logo.png'
|
||||
category = 'twitch, twitchfilm, movie news, movie reviews, cult cinema, independent cinema, anime, foreign cinema, geek talk'
|
||||
language = 'en'
|
||||
|
||||
@ -26,8 +25,8 @@ class Twitchfilm(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
keep_only_tags=[dict(attrs={'class':'asset-header'})]
|
||||
remove_tags_after=dict(attrs={'class':'asset-body'})
|
||||
keep_only_tags=[dict(attrs={'class':'entry'})]
|
||||
remove_tags_after=dict(attrs={'class':'text'})
|
||||
remove_tags = [ dict(name='div', attrs={'class':['social','categories']})
|
||||
, dict(attrs={'id':'main-asset'})
|
||||
, dict(name=['meta','link','iframe','embed','object'])
|
||||
|
@ -64,8 +64,10 @@ class TheWashingtonPost(BasicNewsRecipe):
|
||||
|
||||
def get_article_url(self, article):
|
||||
link = BasicNewsRecipe.get_article_url(self,article)
|
||||
if article.id.startswith('http'):
|
||||
link = article.id
|
||||
if not 'washingtonpost.com' in link:
|
||||
self.log('Skipping adds:', link)
|
||||
self.log('Skipping ads:', link)
|
||||
return None
|
||||
for it in ['_video.html','_gallery.html','_links.html']:
|
||||
if it in link:
|
||||
|
@ -53,9 +53,11 @@
|
||||
|
||||
ul {margin-left: 0}
|
||||
|
||||
.epigraph{width:50%; margin-left : 35%;}
|
||||
.epigraph{width:75%; margin-left : 25%; font-style: italic;}
|
||||
|
||||
div.paragraph { text-indent: 2em; }
|
||||
|
||||
.subtitle { text-align: center; }
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="inline-styles.css" />
|
||||
</head>
|
||||
@ -99,7 +101,7 @@
|
||||
</xsl:template>
|
||||
<!-- secuence template -->
|
||||
<xsl:template name="sequence">
|
||||
<LI/>
|
||||
<li/>
|
||||
<xsl:value-of select="@name"/>
|
||||
<xsl:if test="@number">
|
||||
<xsl:text disable-output-escaping="no">, #</xsl:text>
|
||||
@ -213,7 +215,7 @@
|
||||
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
|
||||
</xsl:element>
|
||||
</xsl:if>
|
||||
<h5>
|
||||
<h5 class="subtitle">
|
||||
<xsl:apply-templates/>
|
||||
</h5>
|
||||
</xsl:template>
|
||||
@ -234,11 +236,11 @@
|
||||
</xsl:template>
|
||||
<!-- strong -->
|
||||
<xsl:template match="fb:strong">
|
||||
<b><xsl:apply-templates/></b>
|
||||
<strong><xsl:apply-templates/></strong>
|
||||
</xsl:template>
|
||||
<!-- emphasis -->
|
||||
<xsl:template match="fb:emphasis">
|
||||
<i> <xsl:apply-templates/></i>
|
||||
<em> <xsl:apply-templates/></em>
|
||||
</xsl:template>
|
||||
<!-- style -->
|
||||
<xsl:template match="fb:style">
|
||||
@ -294,16 +296,30 @@
|
||||
</table>
|
||||
</xsl:template>
|
||||
<xsl:template match="fb:tr">
|
||||
<tr><xsl:apply-templates/></tr>
|
||||
</xsl:template>
|
||||
<xsl:template match="fb:td">
|
||||
<xsl:element name="td">
|
||||
<xsl:element name="tr">
|
||||
<xsl:if test="@align">
|
||||
<xsl:attribute name="align"><xsl:value-of select="@align"/></xsl:attribute>
|
||||
</xsl:if>
|
||||
<xsl:apply-templates/>
|
||||
</xsl:element>
|
||||
</xsl:template>
|
||||
<xsl:template match="fb:td|fb:th">
|
||||
<xsl:element name="{local-name()}">
|
||||
<xsl:if test="@align">
|
||||
<xsl:attribute name="align"><xsl:value-of select="@align"/></xsl:attribute>
|
||||
</xsl:if>
|
||||
<xsl:if test="@style">
|
||||
<xsl:attribute name="style"><xsl:value-of select="@style"/></xsl:attribute>
|
||||
</xsl:if>
|
||||
<xsl:if test="@colspan">
|
||||
<xsl:attribute name="colspan"><xsl:value-of select="@colspan"/></xsl:attribute>
|
||||
</xsl:if>
|
||||
<xsl:if test="@rowspan">
|
||||
<xsl:attribute name="rowspan"><xsl:value-of select="@rowspan"/></xsl:attribute>
|
||||
</xsl:if>
|
||||
<xsl:apply-templates/>
|
||||
</xsl:element>
|
||||
</xsl:template>
|
||||
<!-- epigraph -->
|
||||
<xsl:template match="fb:epigraph">
|
||||
<blockquote class="epigraph">
|
||||
@ -410,5 +426,13 @@
|
||||
</xsl:if>
|
||||
</xsl:element>
|
||||
</xsl:template>
|
||||
<!-- code -->
|
||||
<xsl:template match="fb:code">
|
||||
<code><xsl:apply-templates/></code>
|
||||
</xsl:template>
|
||||
<!-- Strikethrough text -->
|
||||
<xsl:template match="fb:strikethrough">
|
||||
<del><xsl:apply-templates/></del>
|
||||
</xsl:template>
|
||||
|
||||
</xsl:stylesheet>
|
||||
|
@ -191,6 +191,12 @@ if iswindows:
|
||||
# needs_ddk=True,
|
||||
cflags=['/X']
|
||||
),
|
||||
Extension('winfonts',
|
||||
['calibre/utils/fonts/winfonts.cpp'],
|
||||
libraries=['Gdi32', 'User32'],
|
||||
cflags=['/X']
|
||||
),
|
||||
|
||||
])
|
||||
|
||||
if isosx:
|
||||
|
@ -91,7 +91,7 @@ class Plugins(collections.Mapping):
|
||||
'speedup',
|
||||
]
|
||||
if iswindows:
|
||||
plugins.extend(['winutil', 'wpd'])
|
||||
plugins.extend(['winutil', 'wpd', 'winfonts'])
|
||||
if isosx:
|
||||
plugins.append('usbobserver')
|
||||
if islinux or isosx:
|
||||
|
@ -294,6 +294,8 @@ class KINDLE2(KINDLE):
|
||||
|
||||
PRODUCT_ID = [0x0002, 0x0004]
|
||||
BCD = [0x0100]
|
||||
# SUPPORTS_SUB_DIRS = False # Apparently the Paperwhite doesn't like files placed in subdirectories
|
||||
# SUPPORTS_SUB_DIRS_FOR_SCAN = True
|
||||
|
||||
EXTRA_CUSTOMIZATION_MESSAGE = [
|
||||
_('Send page number information when sending books') +
|
||||
|
@ -39,7 +39,7 @@ class KOBO(USBMS):
|
||||
CAN_SET_METADATA = ['collections']
|
||||
|
||||
VENDOR_ID = [0x2237]
|
||||
PRODUCT_ID = [0x4161, 0x4163, 0x4165]
|
||||
PRODUCT_ID = [0x4161, 0x4163, 0x4165, 0x4173, 0x4183]
|
||||
BCD = [0x0110, 0x0323, 0x0326]
|
||||
|
||||
VENDOR_NAME = ['KOBO_INC', 'KOBO']
|
||||
|
@ -155,9 +155,13 @@ class MTP_DEVICE(BASE):
|
||||
# }}}
|
||||
|
||||
# Get list of books from device, with metadata {{{
|
||||
def filesystem_callback(self, msg):
|
||||
self.report_progress(0, msg)
|
||||
|
||||
def books(self, oncard=None, end_session=True):
|
||||
from calibre.devices.mtp.books import JSONCodec
|
||||
from calibre.devices.mtp.books import BookList, Book
|
||||
self.report_progress(0, _('Listing files, this can take a while'))
|
||||
self.get_driveinfo() # Ensure driveinfo is loaded
|
||||
sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard,
|
||||
self._main_id)
|
||||
@ -172,7 +176,7 @@ class MTP_DEVICE(BASE):
|
||||
steps = len(all_books) + 2
|
||||
count = 0
|
||||
|
||||
self.report_progress(0, _('Reading metadata from device'))
|
||||
self.report_progress(0, _('Reading ebook metadata'))
|
||||
# Read the cache if it exists
|
||||
storage = self.filesystem_cache.storage(sid)
|
||||
cache = storage.find_path((self.METADATA_CACHE,))
|
||||
|
@ -239,10 +239,12 @@ class TestDeviceInteraction(unittest.TestCase):
|
||||
|
||||
# Test get_filesystem
|
||||
used_by_one = self.measure_memory_usage(1,
|
||||
self.dev.dev.get_filesystem, self.storage.object_id)
|
||||
self.dev.dev.get_filesystem, self.storage.object_id, lambda x:
|
||||
x)
|
||||
|
||||
used_by_many = self.measure_memory_usage(5,
|
||||
self.dev.dev.get_filesystem, self.storage.object_id)
|
||||
self.dev.dev.get_filesystem, self.storage.object_id, lambda x:
|
||||
x)
|
||||
|
||||
self.check_memory(used_by_one, used_by_many,
|
||||
'Memory consumption during get_filesystem')
|
||||
|
@ -212,6 +212,9 @@ class MTP_DEVICE(MTPDeviceBase):
|
||||
ans += pprint.pformat(storage)
|
||||
return ans
|
||||
|
||||
def _filesystem_callback(self, entry):
|
||||
self.filesystem_callback(_('Found object: %s')%entry.get('name', ''))
|
||||
|
||||
@property
|
||||
def filesystem_cache(self):
|
||||
if self._filesystem_cache is None:
|
||||
@ -231,7 +234,8 @@ class MTP_DEVICE(MTPDeviceBase):
|
||||
storage.append({'id':sid, 'size':capacity,
|
||||
'is_folder':True, 'name':name, 'can_delete':False,
|
||||
'is_system':True})
|
||||
items, errs = self.dev.get_filesystem(sid)
|
||||
items, errs = self.dev.get_filesystem(sid,
|
||||
self._filesystem_callback)
|
||||
all_items.extend(items), all_errs.extend(errs)
|
||||
if not all_items and all_errs:
|
||||
raise DeviceError(
|
||||
|
@ -357,7 +357,7 @@ Device_storage_info(Device *self, void *closure) {
|
||||
|
||||
// Device.get_filesystem {{{
|
||||
|
||||
static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uint32_t parent_id, PyObject *ans, PyObject *errs) {
|
||||
static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uint32_t parent_id, PyObject *ans, PyObject *errs, PyObject *callback) {
|
||||
LIBMTP_file_t *f, *files;
|
||||
PyObject *entry;
|
||||
int ok = 1;
|
||||
@ -372,12 +372,13 @@ static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uin
|
||||
entry = build_file_metadata(f, storage_id);
|
||||
if (entry == NULL) { ok = 0; }
|
||||
else {
|
||||
Py_XDECREF(PyObject_CallFunctionObjArgs(callback, entry, NULL));
|
||||
if (PyList_Append(ans, entry) != 0) { ok = 0; }
|
||||
Py_DECREF(entry);
|
||||
}
|
||||
|
||||
if (ok && f->filetype == LIBMTP_FILETYPE_FOLDER) {
|
||||
if (!recursive_get_files(dev, storage_id, f->item_id, ans, errs)) {
|
||||
if (!recursive_get_files(dev, storage_id, f->item_id, ans, errs, callback)) {
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
@ -394,19 +395,20 @@ static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uin
|
||||
|
||||
static PyObject *
|
||||
Device_get_filesystem(Device *self, PyObject *args) {
|
||||
PyObject *ans, *errs;
|
||||
PyObject *ans, *errs, *callback;
|
||||
unsigned long storage_id;
|
||||
int ok = 0;
|
||||
|
||||
ENSURE_DEV(NULL); ENSURE_STORAGE(NULL);
|
||||
|
||||
if (!PyArg_ParseTuple(args, "k", &storage_id)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "kO", &storage_id, &callback)) return NULL;
|
||||
if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback is not a callable"); return NULL; }
|
||||
ans = PyList_New(0);
|
||||
errs = PyList_New(0);
|
||||
if (errs == NULL || ans == NULL) { PyErr_NoMemory(); return NULL; }
|
||||
|
||||
LIBMTP_Clear_Errorstack(self->device);
|
||||
ok = recursive_get_files(self->device, (uint32_t)storage_id, 0, ans, errs);
|
||||
ok = recursive_get_files(self->device, (uint32_t)storage_id, 0, ans, errs, callback);
|
||||
dump_errorstack(self->device, errs);
|
||||
if (!ok) {
|
||||
Py_DECREF(ans);
|
||||
@ -535,7 +537,7 @@ static PyMethodDef Device_methods[] = {
|
||||
},
|
||||
|
||||
{"get_filesystem", (PyCFunction)Device_get_filesystem, METH_VARARGS,
|
||||
"get_filesystem(storage_id) -> Get the list of files and folders on the device in storage_id. Returns files, errors."
|
||||
"get_filesystem(storage_id, callback) -> Get the list of files and folders on the device in storage_id. Returns files, errors. callback must be a callable that accepts a single argument. It is called with every found object."
|
||||
},
|
||||
|
||||
{"get_file", (PyCFunction)Device_get_file, METH_VARARGS,
|
||||
|
@ -136,8 +136,9 @@ public:
|
||||
HANDLE complete;
|
||||
ULONG self_ref;
|
||||
PyThreadState *thread_state;
|
||||
PyObject *callback;
|
||||
|
||||
GetBulkCallback(PyObject *items_dict, HANDLE ev) : items(items_dict), complete(ev), self_ref(1), thread_state(NULL) {}
|
||||
GetBulkCallback(PyObject *items_dict, HANDLE ev, PyObject* pycallback) : items(items_dict), complete(ev), self_ref(1), thread_state(NULL), callback(pycallback) {}
|
||||
~GetBulkCallback() {}
|
||||
|
||||
HRESULT __stdcall OnStart(REFGUID Context) { return S_OK; }
|
||||
@ -195,6 +196,7 @@ public:
|
||||
Py_DECREF(temp);
|
||||
|
||||
set_properties(obj, properties);
|
||||
Py_XDECREF(PyObject_CallFunctionObjArgs(callback, obj, NULL));
|
||||
|
||||
properties->Release(); properties = NULL;
|
||||
}
|
||||
@ -207,7 +209,7 @@ public:
|
||||
|
||||
};
|
||||
|
||||
static PyObject* bulk_get_filesystem(IPortableDevice *device, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *storage_id, IPortableDevicePropVariantCollection *object_ids) {
|
||||
static PyObject* bulk_get_filesystem(IPortableDevice *device, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *storage_id, IPortableDevicePropVariantCollection *object_ids, PyObject *pycallback) {
|
||||
PyObject *folders = NULL;
|
||||
GUID guid_context = GUID_NULL;
|
||||
HANDLE ev = NULL;
|
||||
@ -227,7 +229,7 @@ static PyObject* bulk_get_filesystem(IPortableDevice *device, IPortableDevicePro
|
||||
properties = create_filesystem_properties_collection();
|
||||
if (properties == NULL) goto end;
|
||||
|
||||
callback = new (std::nothrow) GetBulkCallback(folders, ev);
|
||||
callback = new (std::nothrow) GetBulkCallback(folders, ev, pycallback);
|
||||
if (callback == NULL) { PyErr_NoMemory(); goto end; }
|
||||
|
||||
hr = bulk_properties->QueueGetValuesByObjectList(object_ids, properties, callback, &guid_context);
|
||||
@ -272,7 +274,7 @@ end:
|
||||
// }}}
|
||||
|
||||
// find_all_objects_in() {{{
|
||||
static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevicePropVariantCollection *object_ids, const wchar_t *parent_id) {
|
||||
static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevicePropVariantCollection *object_ids, const wchar_t *parent_id, PyObject *callback) {
|
||||
/*
|
||||
* Find all children of the object identified by parent_id, recursively.
|
||||
* The child ids are put into object_ids. Returns False if any errors
|
||||
@ -284,6 +286,7 @@ static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevice
|
||||
DWORD fetched, i;
|
||||
PROPVARIANT pv;
|
||||
BOOL ok = 1;
|
||||
PyObject *id;
|
||||
|
||||
PropVariantInit(&pv);
|
||||
pv.vt = VT_LPWSTR;
|
||||
@ -303,10 +306,15 @@ static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevice
|
||||
if (SUCCEEDED(hr)) {
|
||||
for(i = 0; i < fetched; i++) {
|
||||
pv.pwszVal = child_ids[i];
|
||||
id = wchar_to_unicode(pv.pwszVal);
|
||||
if (id != NULL) {
|
||||
Py_XDECREF(PyObject_CallFunctionObjArgs(callback, id, NULL));
|
||||
Py_DECREF(id);
|
||||
}
|
||||
hr2 = object_ids->Add(&pv);
|
||||
pv.pwszVal = NULL;
|
||||
if (FAILED(hr2)) { hresult_set_exc("Failed to add child ids to propvariantcollection", hr2); break; }
|
||||
ok = find_all_objects_in(content, object_ids, child_ids[i]);
|
||||
ok = find_all_objects_in(content, object_ids, child_ids[i], callback);
|
||||
if (!ok) break;
|
||||
}
|
||||
for (i = 0; i < fetched; i++) { CoTaskMemFree(child_ids[i]); child_ids[i] = NULL; }
|
||||
@ -347,7 +355,7 @@ end:
|
||||
return ans;
|
||||
}
|
||||
|
||||
static PyObject* single_get_filesystem(IPortableDeviceContent *content, const wchar_t *storage_id, IPortableDevicePropVariantCollection *object_ids) {
|
||||
static PyObject* single_get_filesystem(IPortableDeviceContent *content, const wchar_t *storage_id, IPortableDevicePropVariantCollection *object_ids, PyObject *callback) {
|
||||
DWORD num, i;
|
||||
PROPVARIANT pv;
|
||||
HRESULT hr;
|
||||
@ -375,6 +383,7 @@ static PyObject* single_get_filesystem(IPortableDeviceContent *content, const wc
|
||||
if (SUCCEEDED(hr) && pv.pwszVal != NULL) {
|
||||
item = get_object_properties(devprops, properties, pv.pwszVal);
|
||||
if (item != NULL) {
|
||||
Py_XDECREF(PyObject_CallFunctionObjArgs(callback, item, NULL));
|
||||
PyDict_SetItem(ans, PyDict_GetItemString(item, "id"), item);
|
||||
Py_DECREF(item); item = NULL;
|
||||
ok = 1;
|
||||
@ -429,7 +438,7 @@ end:
|
||||
return values;
|
||||
} // }}}
|
||||
|
||||
PyObject* wpd::get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties) { // {{{
|
||||
PyObject* wpd::get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties, PyObject *callback) { // {{{
|
||||
PyObject *folders = NULL;
|
||||
IPortableDevicePropVariantCollection *object_ids = NULL;
|
||||
IPortableDeviceContent *content = NULL;
|
||||
@ -447,11 +456,11 @@ PyObject* wpd::get_filesystem(IPortableDevice *device, const wchar_t *storage_id
|
||||
Py_END_ALLOW_THREADS;
|
||||
if (FAILED(hr)) { hresult_set_exc("Failed to create propvariantcollection", hr); goto end; }
|
||||
|
||||
ok = find_all_objects_in(content, object_ids, storage_id);
|
||||
ok = find_all_objects_in(content, object_ids, storage_id, callback);
|
||||
if (!ok) goto end;
|
||||
|
||||
if (bulk_properties != NULL) folders = bulk_get_filesystem(device, bulk_properties, storage_id, object_ids);
|
||||
else folders = single_get_filesystem(content, storage_id, object_ids);
|
||||
if (bulk_properties != NULL) folders = bulk_get_filesystem(device, bulk_properties, storage_id, object_ids, callback);
|
||||
else folders = single_get_filesystem(content, storage_id, object_ids, callback);
|
||||
|
||||
end:
|
||||
if (content != NULL) content->Release();
|
||||
|
@ -78,14 +78,15 @@ update_data(Device *self, PyObject *args) {
|
||||
// get_filesystem() {{{
|
||||
static PyObject*
|
||||
py_get_filesystem(Device *self, PyObject *args) {
|
||||
PyObject *storage_id, *ret;
|
||||
PyObject *storage_id, *ret, *callback;
|
||||
wchar_t *storage;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O", &storage_id)) return NULL;
|
||||
if (!PyArg_ParseTuple(args, "OO", &storage_id, &callback)) return NULL;
|
||||
if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback is not a callable"); return NULL; }
|
||||
storage = unicode_to_wchar(storage_id);
|
||||
if (storage == NULL) return NULL;
|
||||
|
||||
ret = wpd::get_filesystem(self->device, storage, self->bulk_properties);
|
||||
ret = wpd::get_filesystem(self->device, storage, self->bulk_properties, callback);
|
||||
free(storage);
|
||||
return ret;
|
||||
} // }}}
|
||||
@ -163,7 +164,7 @@ static PyMethodDef Device_methods[] = {
|
||||
},
|
||||
|
||||
{"get_filesystem", (PyCFunction)py_get_filesystem, METH_VARARGS,
|
||||
"get_filesystem(storage_id) -> Get all files/folders on the storage identified by storage_id. Tries to use bulk operations when possible."
|
||||
"get_filesystem(storage_id, callback) -> Get all files/folders on the storage identified by storage_id. Tries to use bulk operations when possible. callback must be a callable that accepts a single argument. It is called with every found id and then with the metadata for every id."
|
||||
},
|
||||
|
||||
{"get_file", (PyCFunction)py_get_file, METH_VARARGS,
|
||||
|
@ -214,6 +214,14 @@ class MTP_DEVICE(MTPDeviceBase):
|
||||
|
||||
return True
|
||||
|
||||
def _filesystem_callback(self, obj):
|
||||
if isinstance(obj, dict):
|
||||
n = obj.get('name', '')
|
||||
msg = _('Found object: %s')%n
|
||||
else:
|
||||
msg = _('Found id: %s')%obj
|
||||
self.filesystem_callback(msg)
|
||||
|
||||
@property
|
||||
def filesystem_cache(self):
|
||||
if self._filesystem_cache is None:
|
||||
@ -233,7 +241,8 @@ class MTP_DEVICE(MTPDeviceBase):
|
||||
break
|
||||
storage = {'id':storage_id, 'size':capacity, 'name':name,
|
||||
'is_folder':True, 'can_delete':False, 'is_system':True}
|
||||
id_map = self.dev.get_filesystem(storage_id)
|
||||
id_map = self.dev.get_filesystem(storage_id,
|
||||
self._filesystem_callback)
|
||||
for x in id_map.itervalues(): x['storage_id'] = storage_id
|
||||
all_storage.append(storage)
|
||||
items.append(id_map.itervalues())
|
||||
|
@ -56,7 +56,7 @@ int pump_waiting_messages();
|
||||
extern IPortableDeviceValues* get_client_information();
|
||||
extern IPortableDevice* open_device(const wchar_t *pnp_id, IPortableDeviceValues *client_information);
|
||||
extern PyObject* get_device_information(IPortableDevice *device, IPortableDevicePropertiesBulk **bulk_properties);
|
||||
extern PyObject* get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties);
|
||||
extern PyObject* get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties, PyObject *callback);
|
||||
extern PyObject* get_file(IPortableDevice *device, const wchar_t *object_id, PyObject *dest, PyObject *callback);
|
||||
extern PyObject* create_folder(IPortableDevice *device, const wchar_t *parent_id, const wchar_t *name);
|
||||
extern PyObject* delete_object(IPortableDevice *device, const wchar_t *object_id);
|
||||
|
@ -7,7 +7,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re, tempfile, os
|
||||
import re, tempfile, os, imghdr
|
||||
from functools import partial
|
||||
from itertools import izip
|
||||
from urllib import quote
|
||||
@ -247,6 +247,15 @@ class HTMLInput(InputFormatPlugin):
|
||||
if media_type == 'text/plain':
|
||||
self.log.warn('Ignoring link to text file %r'%link_)
|
||||
return None
|
||||
if media_type == self.BINARY_MIME:
|
||||
# Check for the common case, images
|
||||
try:
|
||||
img = imghdr.what(link)
|
||||
except EnvironmentError:
|
||||
pass
|
||||
else:
|
||||
if img:
|
||||
media_type = self.guess_type('dummy.'+img)[0] or self.BINARY_MIME
|
||||
|
||||
self.oeb.log.debug('Added', link)
|
||||
self.oeb.container = self.DirContainer(os.path.dirname(link),
|
||||
|
@ -15,7 +15,6 @@ from calibre.customize.conversion import OutputFormatPlugin, \
|
||||
OptionRecommendation
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
from calibre.constants import iswindows
|
||||
from calibre import walk
|
||||
|
||||
UNITS = [
|
||||
'millimeter',
|
||||
@ -138,6 +137,85 @@ class PDFOutput(OutputFormatPlugin):
|
||||
item = oeb.manifest.ids[cover_id]
|
||||
self.cover_data = item.data
|
||||
|
||||
def handle_embedded_fonts(self):
|
||||
'''
|
||||
Because of QtWebKit's inability to handle embedded fonts correctly, we
|
||||
remove the embedded fonts and make them available system wide instead.
|
||||
If you ever move to Qt WebKit 2.3+ then this will be unnecessary.
|
||||
'''
|
||||
from calibre.ebooks.oeb.base import urlnormalize
|
||||
from calibre.gui2 import must_use_qt
|
||||
from calibre.utils.fonts.utils import get_font_names, remove_embed_restriction
|
||||
from PyQt4.Qt import QFontDatabase, QByteArray
|
||||
|
||||
# First find all @font-face rules and remove them, adding the embedded
|
||||
# fonts to Qt
|
||||
family_map = {}
|
||||
for item in list(self.oeb.manifest):
|
||||
if not hasattr(item.data, 'cssRules'): continue
|
||||
remove = set()
|
||||
for i, rule in enumerate(item.data.cssRules):
|
||||
if rule.type == rule.FONT_FACE_RULE:
|
||||
remove.add(i)
|
||||
try:
|
||||
s = rule.style
|
||||
src = s.getProperty('src').propertyValue[0].uri
|
||||
font_family = s.getProperty('font-family').propertyValue[0].value
|
||||
except:
|
||||
continue
|
||||
path = item.abshref(src)
|
||||
ff = self.oeb.manifest.hrefs.get(urlnormalize(path), None)
|
||||
if ff is None:
|
||||
continue
|
||||
|
||||
raw = ff.data
|
||||
self.oeb.manifest.remove(ff)
|
||||
try:
|
||||
raw = remove_embed_restriction(raw)
|
||||
except:
|
||||
continue
|
||||
must_use_qt()
|
||||
QFontDatabase.addApplicationFontFromData(QByteArray(raw))
|
||||
try:
|
||||
family_name = get_font_names(raw)[0]
|
||||
except:
|
||||
family_name = None
|
||||
if family_name:
|
||||
family_map[icu_lower(font_family)] = family_name
|
||||
|
||||
for i in sorted(remove, reverse=True):
|
||||
item.data.cssRules.pop(i)
|
||||
|
||||
# Now map the font family name specified in the css to the actual
|
||||
# family name of the embedded font (they may be different in general).
|
||||
for item in self.oeb.manifest:
|
||||
if not hasattr(item.data, 'cssRules'): continue
|
||||
for i, rule in enumerate(item.data.cssRules):
|
||||
if rule.type != rule.STYLE_RULE: continue
|
||||
ff = rule.style.getProperty('font-family')
|
||||
if ff is None: continue
|
||||
val = ff.propertyValue
|
||||
for i in xrange(val.length):
|
||||
k = icu_lower(val[i].value)
|
||||
if k in family_map:
|
||||
val[i].value = family_map[k]
|
||||
|
||||
def remove_font_specification(self):
|
||||
# Qt produces image based pdfs on windows when non-generic fonts are specified
|
||||
# This might change in Qt WebKit 2.3+ you will have to test.
|
||||
for item in self.oeb.manifest:
|
||||
if not hasattr(item.data, 'cssRules'): continue
|
||||
for i, rule in enumerate(item.data.cssRules):
|
||||
if rule.type != rule.STYLE_RULE: continue
|
||||
ff = rule.style.getProperty('font-family')
|
||||
if ff is None: continue
|
||||
val = ff.propertyValue
|
||||
for i in xrange(val.length):
|
||||
k = icu_lower(val[i].value)
|
||||
if k not in {'serif', 'sans', 'sans-serif', 'sansserif',
|
||||
'monospace', 'cursive', 'fantasy'}:
|
||||
val[i].value = ''
|
||||
|
||||
def convert_text(self, oeb_book):
|
||||
from calibre.ebooks.pdf.writer import PDFWriter
|
||||
from calibre.ebooks.metadata.opf2 import OPF
|
||||
@ -145,21 +223,16 @@ class PDFOutput(OutputFormatPlugin):
|
||||
self.log.debug('Serializing oeb input to disk for processing...')
|
||||
self.get_cover_data()
|
||||
|
||||
if iswindows:
|
||||
self.remove_font_specification()
|
||||
else:
|
||||
self.handle_embedded_fonts()
|
||||
|
||||
with TemporaryDirectory('_pdf_out') as oeb_dir:
|
||||
from calibre.customize.ui import plugin_for_output_format
|
||||
oeb_output = plugin_for_output_format('oeb')
|
||||
oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log)
|
||||
|
||||
if iswindows:
|
||||
# On windows Qt generates an image based PDF if the html uses
|
||||
# embedded fonts. See https://launchpad.net/bugs/1053906
|
||||
for f in walk(oeb_dir):
|
||||
if f.rpartition('.')[-1].lower() in {'ttf', 'otf'}:
|
||||
self.log.warn('Found embedded font %s, removing it, as '
|
||||
'embedded fonts on windows are not supported by '
|
||||
'the PDF Output plugin'%os.path.basename(f))
|
||||
os.remove(f)
|
||||
|
||||
opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0]
|
||||
opf = OPF(opfpath, os.path.dirname(opfpath))
|
||||
|
||||
|
@ -22,9 +22,10 @@ TEMPLATE = '''
|
||||
li {{ list-style-type: none }}
|
||||
a {{ text-decoration: none }}
|
||||
a:hover {{ color: red }}
|
||||
{extra_css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body id="calibre_generated_inline_toc">
|
||||
<h2>{title}</h2>
|
||||
<ul>
|
||||
</ul>
|
||||
@ -64,7 +65,7 @@ class TOCAdder(object):
|
||||
self.log('\tGenerating in-line ToC')
|
||||
|
||||
root = etree.fromstring(TEMPLATE.format(xhtmlns=XHTML_NS,
|
||||
title=self.title))
|
||||
title=self.title, extra_css=(opts.extra_css or '')))
|
||||
parent = XPath('//h:ul')(root)[0]
|
||||
parent.text = '\n\t'
|
||||
for child in self.oeb.toc:
|
||||
|
@ -12,6 +12,7 @@ from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt,
|
||||
|
||||
ORG_NAME = 'KovidsBrain'
|
||||
APP_UID = 'libprs500'
|
||||
from calibre import prints
|
||||
from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx,
|
||||
plugins, config_dir, filesystem_encoding, DEBUG)
|
||||
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
||||
@ -796,7 +797,8 @@ class Application(QApplication):
|
||||
|
||||
path = os.path.join(sys.extensions_location, 'calibre_style.'+(
|
||||
'pyd' if iswindows else 'so'))
|
||||
self.pi.load_style(path, 'Calibre')
|
||||
if not self.pi.load_style(path, 'Calibre'):
|
||||
prints('Failed to load calibre style')
|
||||
# On OSX, on some machines, colors can be invalid. See https://bugs.launchpad.net/bugs/1014900
|
||||
for role in (orig_pal.Button, orig_pal.Window):
|
||||
c = orig_pal.brush(role).color()
|
||||
@ -853,6 +855,8 @@ class Application(QApplication):
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if not depth_ok:
|
||||
prints('Color depth is less than 32 bits disabling modern look')
|
||||
|
||||
if force_calibre_style or (depth_ok and gprefs['ui_style'] !=
|
||||
'system'):
|
||||
|
@ -286,6 +286,14 @@ class DeleteAction(InterfaceAction):
|
||||
current_row = view.row_count() - 1
|
||||
view.set_current_row(current_row)
|
||||
|
||||
def library_ids_deleted2(self, ids_deleted, next_id=None):
|
||||
view = self.gui.library_view
|
||||
current_row = None
|
||||
if next_id is not None:
|
||||
rmap = view.ids_to_rows([next_id])
|
||||
current_row = rmap.get(next_id, None)
|
||||
self.library_ids_deleted(ids_deleted, current_row=current_row)
|
||||
|
||||
def delete_books(self, *args):
|
||||
'''
|
||||
Delete selected books from device or library.
|
||||
@ -325,16 +333,13 @@ class DeleteAction(InterfaceAction):
|
||||
'removed from your calibre library. Are you sure?')
|
||||
+'</p>', 'library_delete_books', self.gui):
|
||||
return
|
||||
ci = view.currentIndex()
|
||||
row = None
|
||||
if ci.isValid():
|
||||
row = ci.row()
|
||||
next_id = view.next_id
|
||||
if len(rows) < 5:
|
||||
view.model().delete_books_by_id(to_delete_ids)
|
||||
self.library_ids_deleted(to_delete_ids, row)
|
||||
self.library_ids_deleted2(to_delete_ids, next_id=next_id)
|
||||
else:
|
||||
self.__md = MultiDeleter(self.gui, to_delete_ids,
|
||||
partial(self.library_ids_deleted, current_row=row))
|
||||
partial(self.library_ids_deleted2, next_id=next_id))
|
||||
# Device view is visible.
|
||||
else:
|
||||
if self.gui.stack.currentIndex() == 1:
|
||||
|
@ -19,6 +19,7 @@ from calibre.ebooks.conversion.config import load_defaults, \
|
||||
load_specifics, GuiRecommendations
|
||||
from calibre import prepare_string_for_xml
|
||||
from calibre.customize.ui import plugin_for_input_format
|
||||
from calibre.gui2.font_family_chooser import FontFamilyChooser
|
||||
|
||||
def config_widget_for_input_plugin(plugin):
|
||||
name = plugin.name.lower().replace(' ', '_')
|
||||
@ -144,6 +145,8 @@ class Widget(QWidget):
|
||||
return ans
|
||||
elif isinstance(g, QFontComboBox):
|
||||
return unicode(QFontInfo(g.currentFont()).family())
|
||||
elif isinstance(g, FontFamilyChooser):
|
||||
return g.font_family
|
||||
elif isinstance(g, EncodingComboBox):
|
||||
ans = unicode(g.currentText()).strip()
|
||||
try:
|
||||
@ -208,6 +211,8 @@ class Widget(QWidget):
|
||||
getattr(g, 'setCursorPosition', lambda x: x)(0)
|
||||
elif isinstance(g, QFontComboBox):
|
||||
g.setCurrentFont(QFont(val or ''))
|
||||
elif isinstance(g, FontFamilyChooser):
|
||||
g.font_family = val
|
||||
elif isinstance(g, EncodingComboBox):
|
||||
if val:
|
||||
g.setEditText(val)
|
||||
|
@ -6,11 +6,8 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import Qt
|
||||
|
||||
from calibre.gui2.convert.lrf_output_ui import Ui_Form
|
||||
from calibre.gui2.convert import Widget
|
||||
from calibre.gui2.widgets import FontFamilyModel
|
||||
|
||||
font_family_model = None
|
||||
|
||||
@ -30,13 +27,6 @@ class PluginWidget(Widget, Ui_Form):
|
||||
'header_separation', 'minimum_indent']
|
||||
)
|
||||
self.db, self.book_id = db, book_id
|
||||
global font_family_model
|
||||
if font_family_model is None:
|
||||
font_family_model = FontFamilyModel()
|
||||
self.font_family_model = font_family_model
|
||||
self.opt_serif_family.setModel(self.font_family_model)
|
||||
self.opt_sans_family.setModel(self.font_family_model)
|
||||
self.opt_mono_family.setModel(self.font_family_model)
|
||||
|
||||
self.initialize_options(get_option, get_help, db, book_id)
|
||||
self.opt_header.toggle(), self.opt_header.toggle()
|
||||
@ -44,14 +34,4 @@ class PluginWidget(Widget, Ui_Form):
|
||||
self.opt_render_tables_as_images.toggle()
|
||||
|
||||
|
||||
def set_value_handler(self, g, val):
|
||||
if unicode(g.objectName()) in ('opt_serif_family',
|
||||
'opt_sans_family', 'opt_mono_family'):
|
||||
idx = -1
|
||||
if val:
|
||||
idx = g.findText(val, Qt.MatchFixedString)
|
||||
if idx < 0:
|
||||
idx = 0
|
||||
g.setCurrentIndex(idx)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -176,13 +176,13 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="opt_serif_family"/>
|
||||
<widget class="FontFamilyChooser" name="opt_serif_family"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="opt_sans_family"/>
|
||||
<widget class="FontFamilyChooser" name="opt_sans_family"/>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="opt_mono_family"/>
|
||||
<widget class="FontFamilyChooser" name="opt_mono_family"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@ -202,6 +202,13 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>FontFamilyChooser</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>calibre/gui2/font_family_chooser.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
|
@ -101,8 +101,10 @@ class Sendmail(object):
|
||||
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
|
||||
with lopen(attachment, 'rb') as f:
|
||||
msg = compose_mail(from_, to, text, subject, f, aname)
|
||||
efrom, eto = map(extract_email_address, (from_, to))
|
||||
eto = [eto]
|
||||
efrom = extract_email_address(from_)
|
||||
eto = []
|
||||
for x in to.split(','):
|
||||
eto.append(extract_email_address(x.strip()))
|
||||
sendmail(msg, efrom, eto, localhost=None,
|
||||
verbose=1,
|
||||
relay=opts.relay_host,
|
||||
|
168
src/calibre/gui2/font_family_chooser.py
Normal file
168
src/calibre/gui2/font_family_chooser.py
Normal file
@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import (QFontInfo, QFontMetrics, Qt, QFont, QFontDatabase, QPen,
|
||||
QStyledItemDelegate, QSize, QStyle, QComboBox, QStringListModel,
|
||||
QDialog, QVBoxLayout, QApplication, QFontComboBox)
|
||||
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
def writing_system_for_font(font):
|
||||
has_latin = True
|
||||
systems = QFontDatabase().writingSystems(font.family())
|
||||
|
||||
# this just confuses the algorithm below. Vietnamese is Latin with lots of
|
||||
# special chars
|
||||
try:
|
||||
systems.remove(QFontDatabase.Vietnamese)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
system = QFontDatabase.Any
|
||||
|
||||
if (QFontDatabase.Latin not in systems):
|
||||
has_latin = False
|
||||
# we need to show something
|
||||
if systems:
|
||||
system = systems[-1]
|
||||
else:
|
||||
systems.remove(QFontDatabase.Latin)
|
||||
|
||||
if not systems:
|
||||
return system, has_latin
|
||||
|
||||
if (len(systems) == 1 and systems[0] > QFontDatabase.Cyrillic):
|
||||
return systems[0], has_latin
|
||||
|
||||
if (len(systems) <= 2 and
|
||||
systems[-1] > QFontDatabase.Armenian and
|
||||
systems[-1] < QFontDatabase.Vietnamese):
|
||||
return systems[-1], has_latin
|
||||
|
||||
if (len(systems) <= 5 and
|
||||
systems[-1] >= QFontDatabase.SimplifiedChinese and
|
||||
systems[-1] <= QFontDatabase.Korean):
|
||||
system = systems[-1]
|
||||
|
||||
return system, has_latin
|
||||
|
||||
class FontFamilyDelegate(QStyledItemDelegate):
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
text = index.data(Qt.DisplayRole).toString()
|
||||
font = QFont(option.font)
|
||||
font.setPointSize(QFontInfo(font).pointSize() * 1.5)
|
||||
m = QFontMetrics(font)
|
||||
return QSize(m.width(text), m.height())
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
text = unicode(index.data(Qt.DisplayRole).toString())
|
||||
font = QFont(option.font)
|
||||
font.setPointSize(QFontInfo(font).pointSize() * 1.5)
|
||||
font2 = QFont(font)
|
||||
font2.setFamily(text)
|
||||
|
||||
system, has_latin = writing_system_for_font(font2)
|
||||
if has_latin:
|
||||
font = font2
|
||||
|
||||
r = option.rect
|
||||
|
||||
if option.state & QStyle.State_Selected:
|
||||
painter.save()
|
||||
painter.setBrush(option.palette.highlight())
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.drawRect(option.rect)
|
||||
painter.setPen(QPen(option.palette.highlightedText(), 0))
|
||||
|
||||
if (option.direction == Qt.RightToLeft):
|
||||
r.setRight(r.right() - 4)
|
||||
else:
|
||||
r.setLeft(r.left() + 4)
|
||||
|
||||
old = painter.font()
|
||||
painter.setFont(font)
|
||||
painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, text)
|
||||
|
||||
if (system != QFontDatabase.Any):
|
||||
w = painter.fontMetrics().width(text + " ")
|
||||
painter.setFont(font2)
|
||||
sample = QFontDatabase().writingSystemSample(system)
|
||||
if (option.direction == Qt.RightToLeft):
|
||||
r.setRight(r.right() - w)
|
||||
else:
|
||||
r.setLeft(r.left() + w)
|
||||
painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, sample)
|
||||
|
||||
painter.setFont(old)
|
||||
|
||||
if (option.state & QStyle.State_Selected):
|
||||
painter.restore()
|
||||
|
||||
class FontFamilyChooser(QComboBox):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QComboBox.__init__(self, parent)
|
||||
from calibre.utils.fonts import fontconfig
|
||||
try:
|
||||
self.families = fontconfig.find_font_families()
|
||||
except:
|
||||
self.families = []
|
||||
print ('WARNING: Could not load fonts')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# Restrict to Qt families as we need the font to be available in
|
||||
# QFontDatabase
|
||||
qt_families = set([unicode(x) for x in QFontDatabase().families()])
|
||||
self.families = list(qt_families.intersection(set(self.families)))
|
||||
self.families.sort(key=sort_key)
|
||||
self.families.insert(0, _('None'))
|
||||
|
||||
self.m = QStringListModel(self.families)
|
||||
self.setModel(self.m)
|
||||
self.d = FontFamilyDelegate(self)
|
||||
self.setItemDelegate(self.d)
|
||||
self.setCurrentIndex(0)
|
||||
|
||||
def event(self, e):
|
||||
if e.type() == e.Resize:
|
||||
view = self.view()
|
||||
view.window().setFixedWidth(self.width() * 5/3)
|
||||
return QComboBox.event(self, e)
|
||||
|
||||
def sizeHint(self):
|
||||
ans = QComboBox.sizeHint(self)
|
||||
ans.setWidth(QFontMetrics(self.font()).width('m'*14))
|
||||
return ans
|
||||
|
||||
@dynamic_property
|
||||
def font_family(self):
|
||||
def fget(self):
|
||||
idx= self.currentIndex()
|
||||
if idx == 0: return None
|
||||
return self.families[idx]
|
||||
def fset(self, val):
|
||||
if not val:
|
||||
idx = 0
|
||||
try:
|
||||
idx = self.families.index(type(u'')(val))
|
||||
except ValueError:
|
||||
idx = 0
|
||||
self.setCurrentIndex(idx)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication([])
|
||||
d = QDialog()
|
||||
d.setLayout(QVBoxLayout())
|
||||
d.layout().addWidget(FontFamilyChooser(d))
|
||||
d.layout().addWidget(QFontComboBox(d))
|
||||
d.exec_()
|
||||
|
@ -867,6 +867,35 @@ class BooksView(QTableView): # {{{
|
||||
break
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@property
|
||||
def next_id(self):
|
||||
'''
|
||||
Return the id of the 'next' row (i.e. the first unselected row after
|
||||
the current row).
|
||||
'''
|
||||
ci = self.currentIndex()
|
||||
if not ci.isValid():
|
||||
return None
|
||||
selected_rows = frozenset([i.row() for i in self.selectedIndexes() if
|
||||
i.isValid()])
|
||||
column = ci.column()
|
||||
|
||||
for i in xrange(ci.row()+1, self.row_count()):
|
||||
if i in selected_rows: continue
|
||||
try:
|
||||
return self.model().id(self.model().index(i, column))
|
||||
except:
|
||||
pass
|
||||
|
||||
# No unselected rows after the current row, look before
|
||||
for i in xrange(ci.row()-1, -1, -1):
|
||||
if i in selected_rows: continue
|
||||
try:
|
||||
return self.model().id(self.model().index(i, column))
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
self._model.close()
|
||||
|
||||
|
@ -9,10 +9,11 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import textwrap, re, os, errno, shutil
|
||||
|
||||
from PyQt4.Qt import (Qt, QDateTimeEdit, pyqtSignal, QMessageBox,
|
||||
QIcon, QToolButton, QWidget, QLabel, QGridLayout, QApplication,
|
||||
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog, QMenu,
|
||||
QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox, QAction)
|
||||
from PyQt4.Qt import (Qt, QDateTimeEdit, pyqtSignal, QMessageBox, QIcon,
|
||||
QToolButton, QWidget, QLabel, QGridLayout, QApplication,
|
||||
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog, QMenu,
|
||||
QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox,
|
||||
QAction, QCalendarWidget, QDate)
|
||||
|
||||
from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView
|
||||
from calibre.utils.icu import sort_key
|
||||
@ -1371,7 +1372,15 @@ class PublisherEdit(EditWithComplete): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class DateEdit(QDateTimeEdit): # {{{
|
||||
# DateEdit {{{
|
||||
|
||||
class CalendarWidget(QCalendarWidget):
|
||||
|
||||
def showEvent(self, ev):
|
||||
if self.selectedDate().year() == UNDEFINED_DATE.year:
|
||||
self.setSelectedDate(QDate.currentDate())
|
||||
|
||||
class DateEdit(QDateTimeEdit):
|
||||
|
||||
TOOLTIP = ''
|
||||
LABEL = _('&Date:')
|
||||
@ -1388,6 +1397,9 @@ class DateEdit(QDateTimeEdit): # {{{
|
||||
fmt = self.FMT
|
||||
self.setDisplayFormat(fmt)
|
||||
self.setCalendarPopup(True)
|
||||
self.cw = CalendarWidget(self)
|
||||
self.cw.setVerticalHeaderFormat(self.cw.NoVerticalHeader)
|
||||
self.setCalendarWidget(self.cw)
|
||||
self.setMinimumDateTime(UNDEFINED_QDATETIME)
|
||||
self.setSpecialValueText(_('Undefined'))
|
||||
self.clear_button = QToolButton(parent)
|
||||
|
@ -931,6 +931,7 @@ class FullFetch(QDialog): # {{{
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
|
||||
l.addWidget(self.bb)
|
||||
self.bb.rejected.connect(self.reject)
|
||||
self.bb.accepted.connect(self.accept)
|
||||
self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole)
|
||||
self.next_button.setDefault(True)
|
||||
self.next_button.setEnabled(False)
|
||||
@ -978,6 +979,7 @@ class FullFetch(QDialog): # {{{
|
||||
self.log('\n\n')
|
||||
self.covers_widget.start(book, self.current_cover,
|
||||
self.title, self.authors, caches)
|
||||
self.ok_button.setFocus()
|
||||
|
||||
def back_clicked(self):
|
||||
self.next_button.setVisible(True)
|
||||
@ -988,6 +990,8 @@ class FullFetch(QDialog): # {{{
|
||||
self.covers_widget.reset_covers()
|
||||
|
||||
def accept(self):
|
||||
if self.stack.currentIndex() == 1:
|
||||
return QDialog.accept(self)
|
||||
# Prevent the usual dialog accept mechanisms from working
|
||||
pass
|
||||
|
||||
|
@ -21,7 +21,7 @@ from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files,
|
||||
info_dialog, error_dialog, open_url, available_height)
|
||||
from calibre.ebooks.oeb.iterator.book import EbookIterator
|
||||
from calibre.ebooks import DRMError
|
||||
from calibre.constants import islinux, isbsd, isosx, filesystem_encoding
|
||||
from calibre.constants import islinux, isbsd, filesystem_encoding
|
||||
from calibre.utils.config import Config, StringConfig, JSONConfig
|
||||
from calibre.gui2.search_box import SearchBox2
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
@ -209,9 +209,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
||||
self.view_resized_timer.timeout.connect(self.viewport_resize_finished)
|
||||
self.view_resized_timer.setSingleShot(True)
|
||||
self.resize_in_progress = False
|
||||
qs = [Qt.CTRL+Qt.Key_Q]
|
||||
if isosx:
|
||||
qs += [Qt.CTRL+Qt.Key_W]
|
||||
qs = [Qt.CTRL+Qt.Key_Q,Qt.CTRL+Qt.Key_W]
|
||||
self.action_quit.setShortcuts(qs)
|
||||
self.action_quit.triggered.connect(self.quit)
|
||||
self.action_focus_search = QAction(self)
|
||||
|
@ -85,7 +85,7 @@ class Kindle(Device):
|
||||
|
||||
output_profile = 'kindle'
|
||||
output_format = 'MOBI'
|
||||
name = 'Kindle 1-4 and Touch'
|
||||
name = 'Kindle Paperwhite/Touch/1-4'
|
||||
manufacturer = 'Amazon'
|
||||
id = 'kindle'
|
||||
|
||||
|
@ -3,12 +3,13 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Greg Riker'
|
||||
|
||||
import datetime, htmlentitydefs, os, re, shutil, unicodedata, zlib
|
||||
import datetime, htmlentitydefs, os, platform, re, shutil, unicodedata, zlib
|
||||
from copy import deepcopy
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
from calibre import (prepare_string_for_xml, strftime, force_unicode,
|
||||
isbytestring)
|
||||
from calibre.constants import isosx
|
||||
from calibre.customize.conversion import DummyReporter
|
||||
from calibre.customize.ui import output_profiles
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
|
||||
@ -643,12 +644,32 @@ class CatalogBuilder(object):
|
||||
c = item
|
||||
|
||||
ordnum, ordlen = collation_order(c)
|
||||
if last_ordnum != ordnum:
|
||||
last_c = icu_upper(c[0:ordlen])
|
||||
if last_c in exceptions.keys():
|
||||
last_c = exceptions[unicode(last_c)]
|
||||
last_ordnum = ordnum
|
||||
cl_list[idx] = last_c
|
||||
if isosx and platform.mac_ver()[0] < '10.7':
|
||||
# Hackhackhackhackhack
|
||||
# icu returns bogus results with curly apostrophes, maybe others under OS X 10.6.x
|
||||
# When we see the magic combo of 0/-1 for ordnum/ordlen, special case the logic
|
||||
if ordnum == 0 and ordlen == -1:
|
||||
if icu_upper(c[0]) != last_c:
|
||||
last_c = icu_upper(c[0])
|
||||
if last_c in exceptions.keys():
|
||||
last_c = exceptions[unicode(last_c)]
|
||||
last_ordnum = ordnum
|
||||
cl_list[idx] = last_c
|
||||
else:
|
||||
if last_ordnum != ordnum:
|
||||
last_c = icu_upper(c[0:ordlen])
|
||||
if last_c in exceptions.keys():
|
||||
last_c = exceptions[unicode(last_c)]
|
||||
last_ordnum = ordnum
|
||||
cl_list[idx] = last_c
|
||||
|
||||
else:
|
||||
if last_ordnum != ordnum:
|
||||
last_c = icu_upper(c[0:ordlen])
|
||||
if last_c in exceptions.keys():
|
||||
last_c = exceptions[unicode(last_c)]
|
||||
last_ordnum = ordnum
|
||||
cl_list[idx] = last_c
|
||||
|
||||
if self.DEBUG and self.opts.verbose:
|
||||
print(" establish_equivalencies():")
|
||||
@ -656,7 +677,7 @@ class CatalogBuilder(object):
|
||||
for idx, item in enumerate(item_list):
|
||||
print(" %s %s" % (cl_list[idx],item[sort_field]))
|
||||
else:
|
||||
print(" %s %s" % (cl_list[0], item))
|
||||
print(" %s %s" % (cl_list[idx], item))
|
||||
|
||||
return cl_list
|
||||
|
||||
|
@ -6,163 +6,68 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, sys
|
||||
from calibre.constants import iswindows
|
||||
|
||||
from calibre.constants import plugins, iswindows, islinux, isbsd
|
||||
|
||||
_fc, _fc_err = plugins['fontconfig']
|
||||
|
||||
if _fc is None:
|
||||
raise RuntimeError('Failed to load fontconfig with error:'+_fc_err)
|
||||
|
||||
if islinux or isbsd:
|
||||
Thread = object
|
||||
else:
|
||||
from threading import Thread
|
||||
|
||||
class FontConfig(Thread):
|
||||
class Fonts(object):
|
||||
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.failed = False
|
||||
if iswindows:
|
||||
from calibre.utils.fonts.win_fonts import load_winfonts
|
||||
self.backend = load_winfonts()
|
||||
else:
|
||||
from calibre.utils.fonts.fc import fontconfig
|
||||
self.backend = fontconfig
|
||||
|
||||
def run(self):
|
||||
config = None
|
||||
if getattr(sys, 'frameworks_dir', False):
|
||||
config_dir = os.path.join(os.path.dirname(
|
||||
getattr(sys, 'frameworks_dir')), 'Resources', 'fonts')
|
||||
if isinstance(config_dir, unicode):
|
||||
config_dir = config_dir.encode(sys.getfilesystemencoding())
|
||||
config = os.path.join(config_dir, 'fonts.conf')
|
||||
if iswindows and getattr(sys, 'frozen', False):
|
||||
config_dir = os.path.join(os.path.dirname(sys.executable),
|
||||
'fontconfig')
|
||||
if isinstance(config_dir, unicode):
|
||||
config_dir = config_dir.encode(sys.getfilesystemencoding())
|
||||
config = os.path.join(config_dir, 'fonts.conf')
|
||||
try:
|
||||
_fc.initialize(config)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.failed = True
|
||||
|
||||
def wait(self):
|
||||
if not (islinux or isbsd):
|
||||
self.join()
|
||||
if self.failed:
|
||||
raise RuntimeError('Failed to initialize fontconfig')
|
||||
|
||||
def find_font_families(self, allowed_extensions=['ttf', 'otf']):
|
||||
'''
|
||||
Return an alphabetically sorted list of font families available on the system.
|
||||
|
||||
`allowed_extensions`: A list of allowed extensions for font file types. Defaults to
|
||||
`['ttf', 'otf']`. If it is empty, it is ignored.
|
||||
'''
|
||||
self.wait()
|
||||
ans = _fc.find_font_families([bytes('.'+x) for x in allowed_extensions])
|
||||
ans = sorted(set(ans), cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
||||
ans2 = []
|
||||
for x in ans:
|
||||
try:
|
||||
ans2.append(x.decode('utf-8'))
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return ans2
|
||||
def find_font_families(self, allowed_extensions={'ttf', 'otf'}):
|
||||
if iswindows:
|
||||
return self.backend.font_families()
|
||||
return self.backend.find_font_families(allowed_extensions=allowed_extensions)
|
||||
|
||||
def files_for_family(self, family, normalize=True):
|
||||
'''
|
||||
Find all the variants in the font family `family`.
|
||||
Returns a dictionary of tuples. Each tuple is of the form (Full font name, path to font file).
|
||||
Returns a dictionary of tuples. Each tuple is of the form (path to font
|
||||
file, Full font name).
|
||||
The keys of the dictionary depend on `normalize`. If `normalize` is `False`,
|
||||
they are a tuple (slant, weight) otherwise they are strings from the set
|
||||
`('normal', 'bold', 'italic', 'bi', 'light', 'li')`
|
||||
'''
|
||||
self.wait()
|
||||
if isinstance(family, unicode):
|
||||
family = family.encode('utf-8')
|
||||
fonts = {}
|
||||
ofamily = str(family).decode('utf-8')
|
||||
for fullname, path, style, nfamily, weight, slant in \
|
||||
_fc.files_for_family(str(family)):
|
||||
style = (slant, weight)
|
||||
if normalize:
|
||||
italic = slant > 0
|
||||
normal = weight == 80
|
||||
bold = weight > 80
|
||||
if italic:
|
||||
style = 'italic' if normal else 'bi' if bold else 'li'
|
||||
else:
|
||||
style = 'normal' if normal else 'bold' if bold else 'light'
|
||||
try:
|
||||
fullname, path = fullname.decode('utf-8'), path.decode('utf-8')
|
||||
nfamily = nfamily.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if style in fonts:
|
||||
if nfamily.lower().strip() == ofamily.lower().strip() \
|
||||
and 'Condensed' not in fullname and 'ExtraLight' not in fullname:
|
||||
fonts[style] = (path, fullname)
|
||||
else:
|
||||
fonts[style] = (path, fullname)
|
||||
if iswindows:
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
fonts = self.backend.fonts_for_family(family, normalize=normalize)
|
||||
ans = {}
|
||||
for ft, val in fonts.iteritems():
|
||||
ext, name, data = val
|
||||
pt = PersistentTemporaryFile('.'+ext)
|
||||
pt.write(data)
|
||||
pt.close()
|
||||
ans[ft] = (pt.name, name)
|
||||
return ans
|
||||
return self.backend.files_for_family(family, normalize=normalize)
|
||||
|
||||
return fonts
|
||||
|
||||
def match(self, name, all=False, verbose=False):
|
||||
def fonts_for_family(self, family, normalize=True):
|
||||
'''
|
||||
Find the system font that most closely matches `name`, where `name` is a specification
|
||||
of the form::
|
||||
familyname-<pointsize>:<property1=value1>:<property2=value2>...
|
||||
Just like files for family, except that it returns 3-tuples of the form
|
||||
(extension, full name, font data).
|
||||
'''
|
||||
if iswindows:
|
||||
return self.backend.fonts_for_family(family, normalize=normalize)
|
||||
files = self.backend.files_for_family(family, normalize=normalize)
|
||||
ans = {}
|
||||
for ft, val in files.iteritems():
|
||||
name, f = val
|
||||
ext = f.rpartition('.')[-1].lower()
|
||||
ans[ft] = (ext, name, open(f, 'rb').read())
|
||||
return ans
|
||||
|
||||
For example, `verdana:weight=bold:slant=italic`
|
||||
|
||||
Returns a list of dictionaries, or a single dictionary.
|
||||
Each dictionary has the keys:
|
||||
'weight', 'slant', 'family', 'file', 'fullname', 'style'
|
||||
|
||||
`all`: If `True` return a sorted list of matching fonts, where the sort
|
||||
is in order of decreasing closeness of matching. If `False` only the
|
||||
best match is returned. '''
|
||||
self.wait()
|
||||
if isinstance(name, unicode):
|
||||
name = name.encode('utf-8')
|
||||
fonts = []
|
||||
for fullname, path, style, family, weight, slant in \
|
||||
_fc.match(str(name), bool(all), bool(verbose)):
|
||||
try:
|
||||
fullname = fullname.decode('utf-8')
|
||||
path = path.decode('utf-8')
|
||||
style = style.decode('utf-8')
|
||||
family = family.decode('utf-8')
|
||||
fonts.append({
|
||||
'fullname' : fullname,
|
||||
'path' : path,
|
||||
'style' : style,
|
||||
'family' : family,
|
||||
'weight' : weight,
|
||||
'slant' : slant
|
||||
})
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return fonts if all else (fonts[0] if fonts else None)
|
||||
|
||||
fontconfig = FontConfig()
|
||||
if islinux or isbsd:
|
||||
# On X11 Qt also uses fontconfig, so initialization must happen in the
|
||||
# main thread. In any case on X11 initializing fontconfig should be very
|
||||
# fast
|
||||
fontconfig.run()
|
||||
else:
|
||||
fontconfig.start()
|
||||
fontconfig = Fonts()
|
||||
|
||||
def test():
|
||||
from pprint import pprint;
|
||||
pprint(fontconfig.find_font_families())
|
||||
pprint(fontconfig.files_for_family('liberation serif'))
|
||||
import os
|
||||
print(fontconfig.find_font_families())
|
||||
m = 'times new roman' if iswindows else 'liberation serif'
|
||||
pprint(fontconfig.match(m+':slant=italic:weight=bold', verbose=True))
|
||||
for ft, val in fontconfig.files_for_family(m).iteritems():
|
||||
print val[0], ft, val[1], os.path.getsize(val[1])
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
|
@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys, struct
|
||||
|
||||
class UnsupportedFont(ValueError):
|
||||
pass
|
||||
|
||||
def remove_embed_restriction(raw):
|
||||
sfnt_version = raw[:4]
|
||||
if sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO'}:
|
||||
raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sfnt_version)
|
||||
|
||||
num_tables = struct.unpack_from(b'>H', raw, 4)[0]
|
||||
|
||||
# Find OS/2 table
|
||||
offset = 4 + 4*2 # Start of the Table record entries
|
||||
os2_table_offset = None
|
||||
for i in xrange(num_tables):
|
||||
table_tag = raw[offset:offset+4]
|
||||
offset += 16 # Size of a table record
|
||||
if table_tag == b'OS/2':
|
||||
os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0]
|
||||
break
|
||||
if os2_table_offset is None:
|
||||
raise UnsupportedFont('Not a supported font, has no OS/2 table')
|
||||
|
||||
version, = struct.unpack_from(b'>H', raw, os2_table_offset)
|
||||
|
||||
fs_type_offset = os2_table_offset + struct.calcsize(b'>HhHH')
|
||||
fs_type = struct.unpack_from(b'>H', raw, fs_type_offset)[0]
|
||||
if fs_type == 0:
|
||||
return raw
|
||||
|
||||
return raw[:fs_type_offset] + struct.pack(b'>H', 0) + raw[fs_type_offset+2:]
|
||||
|
||||
if __name__ == '__main__':
|
||||
remove_embed_restriction(open(sys.argv[-1], 'rb').read())
|
||||
|
169
src/calibre/utils/fonts/fc.py
Normal file
169
src/calibre/utils/fonts/fc.py
Normal file
@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, sys
|
||||
|
||||
from calibre.constants import plugins, iswindows, islinux, isbsd
|
||||
|
||||
_fc, _fc_err = plugins['fontconfig']
|
||||
|
||||
if _fc is None:
|
||||
raise RuntimeError('Failed to load fontconfig with error:'+_fc_err)
|
||||
|
||||
if islinux or isbsd:
|
||||
Thread = object
|
||||
else:
|
||||
from threading import Thread
|
||||
|
||||
class FontConfig(Thread):
|
||||
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.failed = False
|
||||
|
||||
def run(self):
|
||||
config = None
|
||||
if getattr(sys, 'frameworks_dir', False):
|
||||
config_dir = os.path.join(os.path.dirname(
|
||||
getattr(sys, 'frameworks_dir')), 'Resources', 'fonts')
|
||||
if isinstance(config_dir, unicode):
|
||||
config_dir = config_dir.encode(sys.getfilesystemencoding())
|
||||
config = os.path.join(config_dir, 'fonts.conf')
|
||||
if iswindows and getattr(sys, 'frozen', False):
|
||||
config_dir = os.path.join(os.path.dirname(sys.executable),
|
||||
'fontconfig')
|
||||
if isinstance(config_dir, unicode):
|
||||
config_dir = config_dir.encode(sys.getfilesystemencoding())
|
||||
config = os.path.join(config_dir, 'fonts.conf')
|
||||
try:
|
||||
_fc.initialize(config)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.failed = True
|
||||
|
||||
def wait(self):
|
||||
if not (islinux or isbsd):
|
||||
self.join()
|
||||
if self.failed:
|
||||
raise RuntimeError('Failed to initialize fontconfig')
|
||||
|
||||
def find_font_families(self, allowed_extensions={'ttf', 'otf'}):
|
||||
'''
|
||||
Return an alphabetically sorted list of font families available on the system.
|
||||
|
||||
`allowed_extensions`: A list of allowed extensions for font file types. Defaults to
|
||||
`['ttf', 'otf']`. If it is empty, it is ignored.
|
||||
'''
|
||||
self.wait()
|
||||
ans = _fc.find_font_families([bytes('.'+x) for x in allowed_extensions])
|
||||
ans = sorted(set(ans), cmp=lambda x,y:cmp(x.lower(), y.lower()))
|
||||
ans2 = []
|
||||
for x in ans:
|
||||
try:
|
||||
ans2.append(x.decode('utf-8'))
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return ans2
|
||||
|
||||
def files_for_family(self, family, normalize=True):
|
||||
'''
|
||||
Find all the variants in the font family `family`.
|
||||
Returns a dictionary of tuples. Each tuple is of the form (path to font
|
||||
file, Full font name).
|
||||
The keys of the dictionary depend on `normalize`. If `normalize` is `False`,
|
||||
they are a tuple (slant, weight) otherwise they are strings from the set
|
||||
`('normal', 'bold', 'italic', 'bi', 'light', 'li')`
|
||||
'''
|
||||
self.wait()
|
||||
if isinstance(family, unicode):
|
||||
family = family.encode('utf-8')
|
||||
fonts = {}
|
||||
ofamily = str(family).decode('utf-8')
|
||||
for fullname, path, style, nfamily, weight, slant in \
|
||||
_fc.files_for_family(str(family)):
|
||||
style = (slant, weight)
|
||||
if normalize:
|
||||
italic = slant > 0
|
||||
normal = weight == 80
|
||||
bold = weight > 80
|
||||
if italic:
|
||||
style = 'italic' if normal else 'bi' if bold else 'li'
|
||||
else:
|
||||
style = 'normal' if normal else 'bold' if bold else 'light'
|
||||
try:
|
||||
fullname, path = fullname.decode('utf-8'), path.decode('utf-8')
|
||||
nfamily = nfamily.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
if style in fonts:
|
||||
if nfamily.lower().strip() == ofamily.lower().strip() \
|
||||
and 'Condensed' not in fullname and 'ExtraLight' not in fullname:
|
||||
fonts[style] = (path, fullname)
|
||||
else:
|
||||
fonts[style] = (path, fullname)
|
||||
|
||||
return fonts
|
||||
|
||||
def match(self, name, all=False, verbose=False):
|
||||
'''
|
||||
Find the system font that most closely matches `name`, where `name` is a specification
|
||||
of the form::
|
||||
familyname-<pointsize>:<property1=value1>:<property2=value2>...
|
||||
|
||||
For example, `verdana:weight=bold:slant=italic`
|
||||
|
||||
Returns a list of dictionaries, or a single dictionary.
|
||||
Each dictionary has the keys:
|
||||
'weight', 'slant', 'family', 'file', 'fullname', 'style'
|
||||
|
||||
`all`: If `True` return a sorted list of matching fonts, where the sort
|
||||
is in order of decreasing closeness of matching. If `False` only the
|
||||
best match is returned. '''
|
||||
self.wait()
|
||||
if isinstance(name, unicode):
|
||||
name = name.encode('utf-8')
|
||||
fonts = []
|
||||
for fullname, path, style, family, weight, slant in \
|
||||
_fc.match(str(name), bool(all), bool(verbose)):
|
||||
try:
|
||||
fullname = fullname.decode('utf-8')
|
||||
path = path.decode('utf-8')
|
||||
style = style.decode('utf-8')
|
||||
family = family.decode('utf-8')
|
||||
fonts.append({
|
||||
'fullname' : fullname,
|
||||
'path' : path,
|
||||
'style' : style,
|
||||
'family' : family,
|
||||
'weight' : weight,
|
||||
'slant' : slant
|
||||
})
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return fonts if all else (fonts[0] if fonts else None)
|
||||
|
||||
fontconfig = FontConfig()
|
||||
if islinux or isbsd:
|
||||
# On X11 Qt also uses fontconfig, so initialization must happen in the
|
||||
# main thread. In any case on X11 initializing fontconfig should be very
|
||||
# fast
|
||||
fontconfig.run()
|
||||
else:
|
||||
fontconfig.start()
|
||||
|
||||
def test():
|
||||
from pprint import pprint;
|
||||
pprint(fontconfig.find_font_families())
|
||||
pprint(fontconfig.files_for_family('liberation serif'))
|
||||
m = 'times new roman' if iswindows else 'liberation serif'
|
||||
pprint(fontconfig.match(m+':slant=italic:weight=bold', verbose=True))
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
241
src/calibre/utils/fonts/utils.py
Normal file
241
src/calibre/utils/fonts/utils.py
Normal file
@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import struct
|
||||
from io import BytesIO
|
||||
from collections import defaultdict
|
||||
|
||||
class UnsupportedFont(ValueError):
|
||||
pass
|
||||
|
||||
def is_truetype_font(raw):
|
||||
sfnt_version = raw[:4]
|
||||
return (sfnt_version in {b'\x00\x01\x00\x00', b'OTTO'}, sfnt_version)
|
||||
|
||||
def get_tables(raw):
|
||||
num_tables = struct.unpack_from(b'>H', raw, 4)[0]
|
||||
offset = 4*3 # start of the table record entries
|
||||
for i in xrange(num_tables):
|
||||
table_tag, table_checksum, table_offset, table_length = struct.unpack_from(
|
||||
b'>4s3L', raw, offset)
|
||||
yield (table_tag, raw[table_offset:table_offset+table_length], offset,
|
||||
table_offset, table_checksum)
|
||||
offset += 4*4
|
||||
|
||||
def get_table(raw, name):
|
||||
''' Get the raw table bytes for the specified table in the font '''
|
||||
name = bytes(name.lower())
|
||||
for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw):
|
||||
if table_tag.lower() == name:
|
||||
return table, table_index, table_offset, table_checksum
|
||||
return None, None, None, None
|
||||
|
||||
def get_font_characteristics(raw):
|
||||
'''
|
||||
Return (weight, is_italic, is_bold, is_regular, fs_type). These values are taken
|
||||
from the OS/2 table of the font. See
|
||||
http://www.microsoft.com/typography/otspec/os2.htm for details
|
||||
'''
|
||||
os2_table = get_table(raw, 'os/2')[0]
|
||||
if os2_table is None:
|
||||
raise UnsupportedFont('Not a supported font, has no OS/2 table')
|
||||
|
||||
common_fields = b'>Hh3H11h'
|
||||
(version, char_width, weight, width, fs_type, subscript_x_size,
|
||||
subscript_y_size, subscript_x_offset, subscript_y_offset,
|
||||
superscript_x_size, superscript_y_size, superscript_x_offset,
|
||||
superscript_y_offset, strikeout_size, strikeout_position,
|
||||
family_class) = struct.unpack_from(common_fields, os2_table)
|
||||
offset = struct.calcsize(common_fields)
|
||||
panose = struct.unpack_from(b'>10B', os2_table, offset)
|
||||
panose
|
||||
offset += 10
|
||||
(range1,) = struct.unpack_from(b'>L', os2_table, offset)
|
||||
offset += struct.calcsize(b'>L')
|
||||
if version > 0:
|
||||
range2, range3, range4 = struct.unpack_from(b'>3L', os2_table, offset)
|
||||
offset += struct.calcsize(b'>3L')
|
||||
vendor_id = os2_table[offset:offset+4]
|
||||
vendor_id
|
||||
offset += 4
|
||||
selection, = struct.unpack_from(b'>H', os2_table, offset)
|
||||
|
||||
is_italic = (selection & 0b1) != 0
|
||||
is_bold = (selection & 0b100000) != 0
|
||||
is_regular = (selection & 0b1000000) != 0
|
||||
return weight, is_italic, is_bold, is_regular, fs_type
|
||||
|
||||
def decode_name_record(recs):
|
||||
'''
|
||||
Get the English names of this font. See
|
||||
http://www.microsoft.com/typography/otspec/name.htm for details.
|
||||
'''
|
||||
if not recs: return None
|
||||
unicode_names = {}
|
||||
windows_names = {}
|
||||
mac_names = {}
|
||||
for platform_id, encoding_id, language_id, src in recs:
|
||||
if language_id > 0x8000: continue
|
||||
if platform_id == 0:
|
||||
if encoding_id < 4:
|
||||
try:
|
||||
unicode_names[language_id] = src.decode('utf-16-be')
|
||||
except ValueError:
|
||||
continue
|
||||
elif platform_id == 1:
|
||||
try:
|
||||
mac_names[language_id] = src.decode('utf-8')
|
||||
except ValueError:
|
||||
continue
|
||||
elif platform_id == 2:
|
||||
codec = {0:'ascii', 1:'utf-16-be', 2:'iso-8859-1'}.get(encoding_id,
|
||||
None)
|
||||
if codec is None: continue
|
||||
try:
|
||||
unicode_names[language_id] = src.decode(codec)
|
||||
except ValueError:
|
||||
continue
|
||||
elif platform_id == 3:
|
||||
codec = {1:16, 10:32}.get(encoding_id, None)
|
||||
if codec is None: continue
|
||||
try:
|
||||
windows_names[language_id] = src.decode('utf-%d-be'%codec)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# First try the windows names
|
||||
# First look for the US English name
|
||||
if 1033 in windows_names:
|
||||
return windows_names[1033]
|
||||
# Look for some other english name variant
|
||||
for lang in (3081, 10249, 4105, 9225, 16393, 6153, 8201, 17417, 5129,
|
||||
13321, 18441, 7177, 11273, 2057, 12297):
|
||||
if lang in windows_names:
|
||||
return windows_names[lang]
|
||||
|
||||
# Look for Mac name
|
||||
if 0 in mac_names:
|
||||
return mac_names[0]
|
||||
|
||||
# Use unicode names
|
||||
for val in unicode_names.itervalues():
|
||||
return val
|
||||
|
||||
return None
|
||||
|
||||
def get_font_names(raw):
|
||||
table = get_table(raw, 'name')[0]
|
||||
if table is None:
|
||||
raise UnsupportedFont('Not a supported font, has no name table')
|
||||
table_type, count, string_offset = struct.unpack_from(b'>3H', table)
|
||||
|
||||
records = defaultdict(list)
|
||||
|
||||
for i in xrange(count):
|
||||
try:
|
||||
platform_id, encoding_id, language_id, name_id, length, offset = \
|
||||
struct.unpack_from(b'>6H', table, 6+i*12)
|
||||
except struct.error:
|
||||
break
|
||||
offset += string_offset
|
||||
src = table[offset:offset+length]
|
||||
records[name_id].append((platform_id, encoding_id, language_id,
|
||||
src))
|
||||
|
||||
family_name = decode_name_record(records[1])
|
||||
subfamily_name = decode_name_record(records[2])
|
||||
full_name = decode_name_record(records[4])
|
||||
|
||||
return family_name, subfamily_name, full_name
|
||||
|
||||
def checksum_of_block(raw):
|
||||
extra = 4 - len(raw)%4
|
||||
raw += b'\0'*extra
|
||||
num = len(raw)//4
|
||||
return sum(struct.unpack(b'>%dI'%num, raw)) % (1<<32)
|
||||
|
||||
def verify_checksums(raw):
|
||||
head_table = None
|
||||
for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw):
|
||||
if table_tag.lower() == b'head':
|
||||
version, fontrev, checksum_adj = struct.unpack_from(b'>ffL', table)
|
||||
head_table = table
|
||||
offset = table_offset
|
||||
checksum = table_checksum
|
||||
elif checksum_of_block(table) != table_checksum:
|
||||
raise ValueError('The %r table has an incorrect checksum'%table_tag)
|
||||
|
||||
if head_table is not None:
|
||||
table = head_table
|
||||
table = table[:8] + struct.pack(b'>I', 0) + table[12:]
|
||||
raw = raw[:offset] + table + raw[offset+len(table):]
|
||||
# Check the checksum of the head table
|
||||
if checksum_of_block(table) != checksum:
|
||||
raise ValueError('Checksum of head table not correct')
|
||||
# Check the checksum of the entire font
|
||||
checksum = checksum_of_block(raw)
|
||||
q = (0xB1B0AFBA - checksum) & 0xffffffff
|
||||
if q != checksum_adj:
|
||||
raise ValueError('Checksum of entire font incorrect')
|
||||
|
||||
def set_checksum_adjustment(f):
|
||||
offset = get_table(f.getvalue(), 'head')[2]
|
||||
offset += 8
|
||||
f.seek(offset)
|
||||
f.write(struct.pack(b'>I', 0))
|
||||
checksum = checksum_of_block(f.getvalue())
|
||||
q = (0xB1B0AFBA - checksum) & 0xffffffff
|
||||
f.seek(offset)
|
||||
f.write(struct.pack(b'>I', q))
|
||||
|
||||
def set_table_checksum(f, name):
|
||||
table, table_index, table_offset, table_checksum = get_table(f.getvalue(), name)
|
||||
checksum = checksum_of_block(table)
|
||||
if checksum != table_checksum:
|
||||
f.seek(table_index + 4)
|
||||
f.write(struct.pack(b'>I', checksum))
|
||||
|
||||
def remove_embed_restriction(raw):
|
||||
ok, sig = is_truetype_font(raw)
|
||||
if not ok:
|
||||
raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sig)
|
||||
|
||||
table, table_index, table_offset = get_table(raw, 'os/2')[:3]
|
||||
if table is None:
|
||||
raise UnsupportedFont('Not a supported font, has no OS/2 table')
|
||||
|
||||
fs_type_offset = struct.calcsize(b'>HhHH')
|
||||
fs_type = struct.unpack_from(b'>H', table, fs_type_offset)[0]
|
||||
if fs_type == 0:
|
||||
return raw
|
||||
|
||||
f = BytesIO(raw)
|
||||
f.seek(fs_type_offset + table_offset)
|
||||
f.write(struct.pack(b'>H', 0))
|
||||
|
||||
set_table_checksum(f, 'os/2')
|
||||
set_checksum_adjustment(f)
|
||||
raw = f.getvalue()
|
||||
verify_checksums(raw)
|
||||
return raw
|
||||
|
||||
def test():
|
||||
import sys, os
|
||||
for f in sys.argv[1:]:
|
||||
print (os.path.basename(f))
|
||||
raw = open(f, 'rb').read()
|
||||
print (get_font_names(raw))
|
||||
print (get_font_characteristics(raw))
|
||||
verify_checksums(raw)
|
||||
remove_embed_restriction(raw)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
|
145
src/calibre/utils/fonts/win_fonts.py
Normal file
145
src/calibre/utils/fonts/win_fonts.py
Normal file
@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, sys, atexit
|
||||
from itertools import product
|
||||
|
||||
from calibre import prints, isbytestring
|
||||
from calibre.constants import plugins, filesystem_encoding
|
||||
from calibre.utils.fonts.utils import (is_truetype_font, get_font_names,
|
||||
get_font_characteristics)
|
||||
|
||||
class WinFonts(object):
|
||||
|
||||
def __init__(self, winfonts):
|
||||
self.w = winfonts
|
||||
|
||||
def font_families(self):
|
||||
names = set()
|
||||
for font in self.w.enum_font_families():
|
||||
if (
|
||||
font['is_truetype'] and
|
||||
# Fonts with names starting with @ are designed for
|
||||
# vertical text
|
||||
not font['name'].startswith('@')
|
||||
):
|
||||
names.add(font['name'])
|
||||
return sorted(names)
|
||||
|
||||
def get_normalized_name(self, is_italic, weight):
|
||||
if is_italic:
|
||||
ft = 'bi' if weight == self.w.FW_BOLD else 'italic'
|
||||
else:
|
||||
ft = 'bold' if weight == self.w.FW_BOLD else 'normal'
|
||||
return ft
|
||||
|
||||
def fonts_for_family(self, family, normalize=True):
|
||||
family = type(u'')(family)
|
||||
ans = {}
|
||||
for weight, is_italic in product( (self.w.FW_NORMAL, self.w.FW_BOLD), (False, True) ):
|
||||
try:
|
||||
data = self.w.font_data(family, is_italic, weight)
|
||||
except Exception as e:
|
||||
prints('Failed to get font data for font: %s [%s] with error: %s'%
|
||||
(family, self.get_normalized_name(is_italic, weight), e))
|
||||
continue
|
||||
|
||||
ok, sig = is_truetype_font(data)
|
||||
if not ok:
|
||||
prints('Not a supported font, sfnt_version: %r'%sig)
|
||||
continue
|
||||
ext = 'otf' if sig == b'OTTO' else 'ttf'
|
||||
|
||||
try:
|
||||
weight, is_italic, is_bold, is_regular = get_font_characteristics(data)[:4]
|
||||
except Exception as e:
|
||||
prints('Failed to get font characteristic for font: %s [%s]'
|
||||
' with error: %s'%(family,
|
||||
self.get_normalized_name(is_italic, weight), e))
|
||||
continue
|
||||
|
||||
try:
|
||||
family_name, sub_family_name, full_name = get_font_names(data)
|
||||
except:
|
||||
pass
|
||||
|
||||
if normalize:
|
||||
ft = {(True, True):'bi', (True, False):'italic', (False,
|
||||
True):'bold', (False, False):'normal'}[(is_italic,
|
||||
is_bold)]
|
||||
else:
|
||||
ft = (1 if is_italic else 0, weight//10)
|
||||
|
||||
if not (family_name or full_name):
|
||||
# prints('Font %s [%s] has no names'%(family,
|
||||
# self.get_normalized_name(is_italic, weight)))
|
||||
family_name = family
|
||||
name = full_name or family + ' ' + (sub_family_name or '')
|
||||
|
||||
try:
|
||||
name.encode('ascii')
|
||||
except ValueError:
|
||||
try:
|
||||
sub_family_name.encode('ascii')
|
||||
subf = sub_family_name
|
||||
except:
|
||||
subf = ''
|
||||
|
||||
name = family + ((' ' + subf) if subf else '')
|
||||
|
||||
ans[ft] = (ext, name, data)
|
||||
|
||||
return ans
|
||||
|
||||
def add_system_font(self, path):
|
||||
if isbytestring(path):
|
||||
path = path.decode(filesystem_encoding)
|
||||
path = os.path.abspath(path)
|
||||
ret = self.w.add_system_font(path)
|
||||
if ret > 0:
|
||||
atexit.register(self.remove_system_font, path)
|
||||
return ret
|
||||
|
||||
def remove_system_font(self, path):
|
||||
return self.w.remove_system_font(path)
|
||||
|
||||
def load_winfonts():
|
||||
w, err = plugins['winfonts']
|
||||
if w is None:
|
||||
raise RuntimeError('Failed to load the winfonts module: %s'%err)
|
||||
return WinFonts(w)
|
||||
|
||||
def test_ttf_reading():
|
||||
for f in sys.argv[1:]:
|
||||
raw = open(f).read()
|
||||
print (os.path.basename(f))
|
||||
get_font_characteristics(raw)
|
||||
print()
|
||||
|
||||
if __name__ == '__main__':
|
||||
base = os.path.abspath(__file__)
|
||||
d = os.path.dirname
|
||||
pluginsd = os.path.join(d(d(d(base))), 'plugins')
|
||||
if os.path.exists(os.path.join(pluginsd, 'winfonts.pyd')):
|
||||
sys.path.insert(0, pluginsd)
|
||||
import winfonts
|
||||
w = WinFonts(winfonts)
|
||||
else:
|
||||
w = load_winfonts()
|
||||
|
||||
print (w.w)
|
||||
families = w.font_families()
|
||||
print (families)
|
||||
|
||||
for family in families:
|
||||
prints(family + ':')
|
||||
for font, data in w.fonts_for_family(family).iteritems():
|
||||
prints(' ', font, data[0], data[1], len(data[2]))
|
||||
print ()
|
||||
|
@ -1,168 +1,263 @@
|
||||
/*
|
||||
:mod:`fontconfig` -- Pythonic interface to Windows font api
|
||||
:mod:`winfont` -- Pythonic interface to Windows font api
|
||||
============================================================
|
||||
|
||||
.. module:: fontconfig
|
||||
.. module:: winfonts
|
||||
:platform: All
|
||||
:synopsis: Pythonic interface to the fontconfig library
|
||||
:synopsis: Pythonic interface to the windows font routines
|
||||
|
||||
.. moduleauthor:: Kovid Goyal <kovid@kovidgoyal.net> Copyright 2009
|
||||
|
||||
*/
|
||||
|
||||
#define _UNICODE
|
||||
#define UNICODE
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Windows.h>
|
||||
#include <strsafe.h>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
vector<BYTE> *get_font_data(HDC hdc) {
|
||||
DWORD sz;
|
||||
vector<BYTE> *data;
|
||||
sz = GetFontData(hdc, 0, 0, NULL, 0);
|
||||
data = new vector<BYTE>(sz);
|
||||
if (GetFontData(hdc, 0, 0, &((*data)[0]), sz) == GDI_ERROR) {
|
||||
delete data; data = NULL;
|
||||
}
|
||||
return data;
|
||||
#include <Strsafe.h>
|
||||
#include <Python.h>
|
||||
#include <new>
|
||||
|
||||
// Utils {{{
|
||||
static wchar_t* unicode_to_wchar(PyObject *o) {
|
||||
wchar_t *buf;
|
||||
Py_ssize_t len;
|
||||
if (o == NULL) return NULL;
|
||||
if (!PyUnicode_Check(o)) {PyErr_Format(PyExc_TypeError, "The python object must be a unicode object"); return NULL;}
|
||||
len = PyUnicode_GET_SIZE(o);
|
||||
buf = (wchar_t *)calloc(len+2, sizeof(wchar_t));
|
||||
if (buf == NULL) { PyErr_NoMemory(); return NULL; }
|
||||
len = PyUnicode_AsWideChar((PyUnicodeObject*)o, buf, len);
|
||||
if (len == -1) { free(buf); PyErr_Format(PyExc_TypeError, "Invalid python unicode object."); return NULL; }
|
||||
return buf;
|
||||
}
|
||||
|
||||
BOOL is_font_embeddable(ENUMLOGFONTEX *lpelfe) {
|
||||
HDC hdc;
|
||||
HFONT font;
|
||||
HFONT old_font = NULL;
|
||||
UINT sz;
|
||||
size_t i;
|
||||
LPOUTLINETEXTMETRICW metrics;
|
||||
BOOL ans = TRUE;
|
||||
hdc = GetDC(NULL);
|
||||
font = CreateFontIndirect(&lpelfe->elfLogFont);
|
||||
if (font != NULL) {
|
||||
old_font = SelectObject(hdc, font);
|
||||
sz = GetOutlineTextMetrics(hdc, 0, NULL);
|
||||
metrics = new OUTLINETEXTMETRICW[sz];
|
||||
if ( GetOutlineTextMetrics(hdc, sz, metrics) != 0) {
|
||||
for ( i = 0; i < sz; i++) {
|
||||
if (metrics[i].otmfsType & 0x01) {
|
||||
wprintf_s(L"Not embeddable: %s\n", lpelfe->elfLogFont.lfFaceName);
|
||||
ans = FALSE; break;
|
||||
}
|
||||
}
|
||||
} else ans = FALSE;
|
||||
delete[] metrics;
|
||||
DeleteObject(font);
|
||||
SelectObject(hdc, old_font);
|
||||
} else ans = FALSE;
|
||||
ReleaseDC(NULL, hdc);
|
||||
static PyObject* wchar_to_unicode(const wchar_t *o) {
|
||||
PyObject *ans;
|
||||
if (o == NULL) return NULL;
|
||||
ans = PyUnicode_FromWideChar(o, wcslen(o));
|
||||
if (ans == NULL) PyErr_NoMemory();
|
||||
return ans;
|
||||
}
|
||||
|
||||
int CALLBACK find_families_callback (
|
||||
ENUMLOGFONTEX *lpelfe, /* pointer to logical-font data */
|
||||
NEWTEXTMETRICEX *lpntme, /* pointer to physical-font data */
|
||||
int FontType, /* type of font */
|
||||
LPARAM lParam /* a combo box HWND */
|
||||
) {
|
||||
size_t i;
|
||||
LPWSTR tmp;
|
||||
vector<LPWSTR> *families = (vector<LPWSTR>*)lParam;
|
||||
// }}}
|
||||
|
||||
if (FontType & TRUETYPE_FONTTYPE) {
|
||||
for (i = 0; i < families->size(); i++) {
|
||||
if (lstrcmp(families->at(i), lpelfe->elfLogFont.lfFaceName) == 0)
|
||||
return 1;
|
||||
}
|
||||
tmp = new WCHAR[LF_FACESIZE];
|
||||
swprintf_s(tmp, LF_FACESIZE, L"%s", lpelfe->elfLogFont.lfFaceName);
|
||||
families->push_back(tmp);
|
||||
}
|
||||
// Enumerate font families {{{
|
||||
struct EnumData {
|
||||
HDC hdc;
|
||||
PyObject *families;
|
||||
};
|
||||
|
||||
|
||||
static PyObject* logfont_to_dict(const ENUMLOGFONTEX *lf, const TEXTMETRIC *tm, DWORD font_type, HDC hdc) {
|
||||
PyObject *name, *full_name, *style, *script;
|
||||
LOGFONT f = lf->elfLogFont;
|
||||
|
||||
name = wchar_to_unicode(f.lfFaceName);
|
||||
full_name = wchar_to_unicode(lf->elfFullName);
|
||||
style = wchar_to_unicode(lf->elfStyle);
|
||||
script = wchar_to_unicode(lf->elfScript);
|
||||
|
||||
return Py_BuildValue("{s:N, s:N, s:N, s:N, s:O, s:O, s:O, s:O, s:l}",
|
||||
"name", name,
|
||||
"full_name", full_name,
|
||||
"style", style,
|
||||
"script", script,
|
||||
"is_truetype", (font_type & TRUETYPE_FONTTYPE) ? Py_True : Py_False,
|
||||
"is_italic", (tm->tmItalic != 0) ? Py_True : Py_False,
|
||||
"is_underlined", (tm->tmUnderlined != 0) ? Py_True : Py_False,
|
||||
"is_strikeout", (tm->tmStruckOut != 0) ? Py_True : Py_False,
|
||||
"weight", tm->tmWeight
|
||||
);
|
||||
}
|
||||
|
||||
static int CALLBACK find_families_callback(const ENUMLOGFONTEX *lpelfe, const TEXTMETRIC *lpntme, DWORD font_type, LPARAM lParam) {
|
||||
struct EnumData *enum_data = reinterpret_cast<struct EnumData*>(lParam);
|
||||
PyObject *font = logfont_to_dict(lpelfe, lpntme, font_type, enum_data->hdc);
|
||||
if (font == NULL) return 0;
|
||||
PyList_Append(enum_data->families, font);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
vector<LPWSTR>* find_font_families(void) {
|
||||
static PyObject* enum_font_families(PyObject *self, PyObject *args) {
|
||||
LOGFONTW logfont;
|
||||
HDC hdc;
|
||||
vector<LPWSTR> *families;
|
||||
PyObject *families;
|
||||
struct EnumData enum_data;
|
||||
|
||||
families = new vector<LPWSTR>();
|
||||
families = PyList_New(0);
|
||||
if (families == NULL) return PyErr_NoMemory();
|
||||
SecureZeroMemory(&logfont, sizeof(logfont));
|
||||
|
||||
logfont.lfCharSet = DEFAULT_CHARSET;
|
||||
logfont.lfPitchAndFamily = VARIABLE_PITCH | FF_DONTCARE;
|
||||
StringCchCopyW(logfont.lfFaceName, 2, L"\0");
|
||||
logfont.lfFaceName[0] = L'\0';
|
||||
|
||||
hdc = GetDC(NULL);
|
||||
EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback,
|
||||
(LPARAM)(families), 0);
|
||||
enum_data.hdc = hdc;
|
||||
enum_data.families = families;
|
||||
|
||||
EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback,
|
||||
(LPARAM)(&enum_data), 0);
|
||||
ReleaseDC(NULL, hdc);
|
||||
|
||||
return families;
|
||||
}
|
||||
|
||||
inline void free_families_vector(vector<LPWSTR> *v) {
|
||||
for (size_t i = 0; i < v->size(); i++) delete[] v->at(i);
|
||||
delete v;
|
||||
// }}}
|
||||
|
||||
// font_data() {{{
|
||||
static PyObject* font_data(PyObject *self, PyObject *args) {
|
||||
PyObject *ans = NULL, *italic, *pyname;
|
||||
LOGFONTW lf;
|
||||
HDC hdc;
|
||||
LONG weight;
|
||||
LPWSTR family = NULL;
|
||||
HGDIOBJ old_font = NULL;
|
||||
HFONT hf;
|
||||
DWORD sz;
|
||||
char *buf;
|
||||
|
||||
SecureZeroMemory(&lf, sizeof(lf));
|
||||
|
||||
if (!PyArg_ParseTuple(args, "OOl", &pyname, &italic, &weight)) return NULL;
|
||||
|
||||
family = unicode_to_wchar(pyname);
|
||||
if (family == NULL) { Py_DECREF(ans); return NULL; }
|
||||
StringCchCopyW(lf.lfFaceName, LF_FACESIZE, family);
|
||||
free(family);
|
||||
|
||||
lf.lfItalic = (PyObject_IsTrue(italic)) ? 1 : 0;
|
||||
lf.lfWeight = weight;
|
||||
lf.lfOutPrecision = OUT_TT_ONLY_PRECIS;
|
||||
|
||||
hdc = GetDC(NULL);
|
||||
|
||||
if ( (hf = CreateFontIndirect(&lf)) != NULL) {
|
||||
|
||||
if ( (old_font = SelectObject(hdc, hf)) != NULL ) {
|
||||
sz = GetFontData(hdc, 0, 0, NULL, 0);
|
||||
if (sz != GDI_ERROR) {
|
||||
buf = (char*)calloc(sz, sizeof(char));
|
||||
|
||||
if (buf != NULL) {
|
||||
if (GetFontData(hdc, 0, 0, buf, sz) != GDI_ERROR) {
|
||||
ans = PyBytes_FromStringAndSize(buf, sz);
|
||||
if (ans == NULL) PyErr_NoMemory();
|
||||
} else PyErr_SetString(PyExc_ValueError, "GDI Error");
|
||||
free(buf);
|
||||
} else PyErr_NoMemory();
|
||||
} else PyErr_SetString(PyExc_ValueError, "GDI Error");
|
||||
|
||||
SelectObject(hdc, old_font);
|
||||
} else PyErr_SetFromWindowsErr(0);
|
||||
DeleteObject(hf);
|
||||
} else PyErr_SetFromWindowsErr(0);
|
||||
|
||||
ReleaseDC(NULL, hdc);
|
||||
|
||||
return ans;
|
||||
}
|
||||
// }}}
|
||||
|
||||
static PyObject* add_font(PyObject *self, PyObject *args) {
|
||||
char *data;
|
||||
Py_ssize_t sz;
|
||||
DWORD num = 0;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "s#", &data, &sz)) return NULL;
|
||||
|
||||
AddFontMemResourceEx(data, sz, NULL, &num);
|
||||
|
||||
return Py_BuildValue("k", num);
|
||||
}
|
||||
|
||||
#ifdef TEST
|
||||
static PyObject* add_system_font(PyObject *self, PyObject *args) {
|
||||
PyObject *name;
|
||||
LPWSTR path;
|
||||
int num;
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
vector<LPWSTR> *all_families;
|
||||
size_t i;
|
||||
if (!PyArg_ParseTuple(args, "O", &name)) return NULL;
|
||||
path = unicode_to_wchar(name);
|
||||
if (path == NULL) return NULL;
|
||||
|
||||
all_families = find_font_families();
|
||||
|
||||
for (i = 0; i < all_families->size(); i++)
|
||||
wprintf_s(L"%s\n", all_families->at(i));
|
||||
|
||||
free_families_vector(all_families);
|
||||
|
||||
HDC hdc = GetDC(NULL);
|
||||
HFONT font = CreateFont(72,0,0,0,0,0,0,0,0,0,0,0,0,L"Verdana");
|
||||
HFONT old_font = SelectObject(hdc, font);
|
||||
vector<BYTE> *data = get_font_data(hdc);
|
||||
DeleteObject(font);
|
||||
SelectObject(hdc, old_font);
|
||||
ReleaseDC(NULL, hdc);
|
||||
if (data != NULL) printf("\nyay: %d\n", data->size());
|
||||
delete data;
|
||||
|
||||
return 0;
|
||||
num = AddFontResource(path);
|
||||
if (num > 0)
|
||||
SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
|
||||
free(path);
|
||||
return Py_BuildValue("i", num);
|
||||
}
|
||||
#else
|
||||
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#
|
||||
static PyObject* remove_system_font(PyObject *self, PyObject *args) {
|
||||
PyObject *name, *ok = Py_False;
|
||||
LPWSTR path;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "O", &name)) return NULL;
|
||||
path = unicode_to_wchar(name);
|
||||
if (path == NULL) return NULL;
|
||||
|
||||
if (RemoveFontResource(path)) {
|
||||
SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
|
||||
ok = Py_True;
|
||||
}
|
||||
free(path);
|
||||
return Py_BuildValue("O", ok);
|
||||
}
|
||||
|
||||
static
|
||||
PyMethodDef fontconfig_methods[] = {
|
||||
{"find_font_families", fontconfig_find_font_families, METH_VARARGS,
|
||||
"find_font_families(allowed_extensions)\n\n"
|
||||
"Find all font families on the system for fonts of the specified types. If no "
|
||||
"types are specified all font families are returned."
|
||||
PyMethodDef winfonts_methods[] = {
|
||||
{"enum_font_families", enum_font_families, METH_VARARGS,
|
||||
"enum_font_families()\n\n"
|
||||
"Enumerate all regular (not italic/bold/etc. variants) font families on the system. Note there will be multiple entries for every family (corresponding to each charset of the font)."
|
||||
},
|
||||
|
||||
{"font_data", font_data, METH_VARARGS,
|
||||
"font_data(family_name, italic, weight)\n\n"
|
||||
"Return the raw font data for the specified font."
|
||||
},
|
||||
|
||||
{"add_font", add_font, METH_VARARGS,
|
||||
"add_font(data)\n\n"
|
||||
"Add the font(s) in the data (bytestring) to windows. Added fonts are always private. Returns the number of fonts added."
|
||||
},
|
||||
|
||||
{"add_system_font", add_system_font, METH_VARARGS,
|
||||
"add_system_font(data)\n\n"
|
||||
"Add the font(s) in the specified file to the system font tables."
|
||||
},
|
||||
|
||||
{"remove_system_font", remove_system_font, METH_VARARGS,
|
||||
"remove_system_font(data)\n\n"
|
||||
"Remove the font(s) in the specified file from the system font tables."
|
||||
},
|
||||
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
||||
extern "C" {
|
||||
PyMODINIT_FUNC
|
||||
initfontconfig(void) {
|
||||
initwinfonts(void) {
|
||||
PyObject *m;
|
||||
m = Py_InitModule3(
|
||||
"fontconfig", fontconfig_methods,
|
||||
"Find fonts."
|
||||
"winfonts", winfonts_methods,
|
||||
"Windows font API"
|
||||
);
|
||||
if (m == NULL) return;
|
||||
}
|
||||
|
||||
PyModule_AddIntMacro(m, FW_DONTCARE);
|
||||
PyModule_AddIntMacro(m, FW_THIN);
|
||||
PyModule_AddIntMacro(m, FW_EXTRALIGHT);
|
||||
PyModule_AddIntMacro(m, FW_ULTRALIGHT);
|
||||
PyModule_AddIntMacro(m, FW_LIGHT);
|
||||
PyModule_AddIntMacro(m, FW_NORMAL);
|
||||
PyModule_AddIntMacro(m, FW_REGULAR);
|
||||
PyModule_AddIntMacro(m, FW_MEDIUM);
|
||||
PyModule_AddIntMacro(m, FW_SEMIBOLD);
|
||||
PyModule_AddIntMacro(m, FW_DEMIBOLD);
|
||||
PyModule_AddIntMacro(m, FW_BOLD);
|
||||
PyModule_AddIntMacro(m, FW_EXTRABOLD);
|
||||
PyModule_AddIntMacro(m, FW_ULTRABOLD);
|
||||
PyModule_AddIntMacro(m, FW_HEAVY);
|
||||
PyModule_AddIntMacro(m, FW_BLACK);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
Loading…
x
Reference in New Issue
Block a user