Merge from trunk

This commit is contained in:
Charles Haley 2012-10-03 07:22:42 +02:00
commit f386fcb539
44 changed files with 1302 additions and 494 deletions

View File

@ -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 |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 Content From The Web

View File

@ -6,40 +6,19 @@ www.foreignpolicy.com
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class ForeignPolicy(BasicNewsRecipe): class AdvancedUserRecipe1349086293(BasicNewsRecipe):
title = 'Foreign Policy' title = u'Foreign Policy'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'International News' description = 'International News'
publisher = 'Washingtonpost.Newsweek Interactive, LLC' publisher = 'Washingtonpost.Newsweek Interactive, LLC'
category = 'news, politics, USA' category = 'news, politics, USA'
oldest_article = 31 oldest_article = 31
max_articles_per_feed = 200 max_articles_per_feed = 200
no_stylesheets = True auto_cleanup = 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} '
conversion_options = { feeds = [(u'Foreign_Policy', u'http://www.foreignpolicy.com/node/feed')]
'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')]
def print_version(self, url): 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

View File

@ -11,23 +11,8 @@ class NatureNews(BasicNewsRecipe):
max_articles_per_feed = 50 max_articles_per_feed = 50
no_stylesheets = True no_stylesheets = True
keep_only_tags = [dict(name='div', attrs={'id':'content'})] use_embedded_content = False
# remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'}) keep_only_tags = [dict(name='div', attrs={'id':'article'})]
# 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: ''),
]
extra_css = ''' extra_css = '''
.author { text-align: right; font-size: small; line-height:1em; margin-top:0px; margin-left:0; margin-right:0; margin-bottom: 0; } .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; } .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')] 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -1,5 +1,5 @@
__license__ = 'GPL v3' __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/ twitchfilm.net/news/
''' '''
@ -15,7 +15,6 @@ class Twitchfilm(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
encoding = 'utf-8' encoding = 'utf-8'
publisher = 'Twitch' 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' category = 'twitch, twitchfilm, movie news, movie reviews, cult cinema, independent cinema, anime, foreign cinema, geek talk'
language = 'en' language = 'en'
@ -26,8 +25,8 @@ class Twitchfilm(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
keep_only_tags=[dict(attrs={'class':'asset-header'})] keep_only_tags=[dict(attrs={'class':'entry'})]
remove_tags_after=dict(attrs={'class':'asset-body'}) remove_tags_after=dict(attrs={'class':'text'})
remove_tags = [ dict(name='div', attrs={'class':['social','categories']}) remove_tags = [ dict(name='div', attrs={'class':['social','categories']})
, dict(attrs={'id':'main-asset'}) , dict(attrs={'id':'main-asset'})
, dict(name=['meta','link','iframe','embed','object']) , dict(name=['meta','link','iframe','embed','object'])

View File

@ -64,8 +64,10 @@ class TheWashingtonPost(BasicNewsRecipe):
def get_article_url(self, article): def get_article_url(self, article):
link = BasicNewsRecipe.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: if not 'washingtonpost.com' in link:
self.log('Skipping adds:', link) self.log('Skipping ads:', link)
return None return None
for it in ['_video.html','_gallery.html','_links.html']: for it in ['_video.html','_gallery.html','_links.html']:
if it in link: if it in link:

View File

@ -53,9 +53,11 @@
ul {margin-left: 0} ul {margin-left: 0}
.epigraph{width:50%; margin-left : 35%;} .epigraph{width:75%; margin-left : 25%; font-style: italic;}
div.paragraph { text-indent: 2em; } div.paragraph { text-indent: 2em; }
.subtitle { text-align: center; }
</style> </style>
<link rel="stylesheet" type="text/css" href="inline-styles.css" /> <link rel="stylesheet" type="text/css" href="inline-styles.css" />
</head> </head>
@ -99,7 +101,7 @@
</xsl:template> </xsl:template>
<!-- secuence template --> <!-- secuence template -->
<xsl:template name="sequence"> <xsl:template name="sequence">
<LI/> <li/>
<xsl:value-of select="@name"/> <xsl:value-of select="@name"/>
<xsl:if test="@number"> <xsl:if test="@number">
<xsl:text disable-output-escaping="no">,&#032;#</xsl:text> <xsl:text disable-output-escaping="no">,&#032;#</xsl:text>
@ -213,7 +215,7 @@
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute> <xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element> </xsl:element>
</xsl:if> </xsl:if>
<h5> <h5 class="subtitle">
<xsl:apply-templates/> <xsl:apply-templates/>
</h5> </h5>
</xsl:template> </xsl:template>
@ -234,11 +236,11 @@
</xsl:template> </xsl:template>
<!-- strong --> <!-- strong -->
<xsl:template match="fb:strong"> <xsl:template match="fb:strong">
<b><xsl:apply-templates/></b> <strong><xsl:apply-templates/></strong>
</xsl:template> </xsl:template>
<!-- emphasis --> <!-- emphasis -->
<xsl:template match="fb:emphasis"> <xsl:template match="fb:emphasis">
<i> <xsl:apply-templates/></i> <em> <xsl:apply-templates/></em>
</xsl:template> </xsl:template>
<!-- style --> <!-- style -->
<xsl:template match="fb:style"> <xsl:template match="fb:style">
@ -294,16 +296,30 @@
</table> </table>
</xsl:template> </xsl:template>
<xsl:template match="fb:tr"> <xsl:template match="fb:tr">
<tr><xsl:apply-templates/></tr> <xsl:element name="tr">
</xsl:template>
<xsl:template match="fb:td">
<xsl:element name="td">
<xsl:if test="@align"> <xsl:if test="@align">
<xsl:attribute name="align"><xsl:value-of select="@align"/></xsl:attribute> <xsl:attribute name="align"><xsl:value-of select="@align"/></xsl:attribute>
</xsl:if> </xsl:if>
<xsl:apply-templates/> <xsl:apply-templates/>
</xsl:element> </xsl:element>
</xsl:template> </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 --> <!-- epigraph -->
<xsl:template match="fb:epigraph"> <xsl:template match="fb:epigraph">
<blockquote class="epigraph"> <blockquote class="epigraph">
@ -410,5 +426,13 @@
</xsl:if> </xsl:if>
</xsl:element> </xsl:element>
</xsl:template> </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> </xsl:stylesheet>

View File

@ -191,6 +191,12 @@ if iswindows:
# needs_ddk=True, # needs_ddk=True,
cflags=['/X'] cflags=['/X']
), ),
Extension('winfonts',
['calibre/utils/fonts/winfonts.cpp'],
libraries=['Gdi32', 'User32'],
cflags=['/X']
),
]) ])
if isosx: if isosx:

View File

@ -91,7 +91,7 @@ class Plugins(collections.Mapping):
'speedup', 'speedup',
] ]
if iswindows: if iswindows:
plugins.extend(['winutil', 'wpd']) plugins.extend(['winutil', 'wpd', 'winfonts'])
if isosx: if isosx:
plugins.append('usbobserver') plugins.append('usbobserver')
if islinux or isosx: if islinux or isosx:

View File

@ -294,6 +294,8 @@ class KINDLE2(KINDLE):
PRODUCT_ID = [0x0002, 0x0004] PRODUCT_ID = [0x0002, 0x0004]
BCD = [0x0100] 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 = [ EXTRA_CUSTOMIZATION_MESSAGE = [
_('Send page number information when sending books') + _('Send page number information when sending books') +

View File

@ -39,7 +39,7 @@ class KOBO(USBMS):
CAN_SET_METADATA = ['collections'] CAN_SET_METADATA = ['collections']
VENDOR_ID = [0x2237] VENDOR_ID = [0x2237]
PRODUCT_ID = [0x4161, 0x4163, 0x4165] PRODUCT_ID = [0x4161, 0x4163, 0x4165, 0x4173, 0x4183]
BCD = [0x0110, 0x0323, 0x0326] BCD = [0x0110, 0x0323, 0x0326]
VENDOR_NAME = ['KOBO_INC', 'KOBO'] VENDOR_NAME = ['KOBO_INC', 'KOBO']

View File

@ -155,9 +155,13 @@ class MTP_DEVICE(BASE):
# }}} # }}}
# Get list of books from device, with metadata {{{ # 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): def books(self, oncard=None, end_session=True):
from calibre.devices.mtp.books import JSONCodec from calibre.devices.mtp.books import JSONCodec
from calibre.devices.mtp.books import BookList, Book 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 self.get_driveinfo() # Ensure driveinfo is loaded
sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard, sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard,
self._main_id) self._main_id)
@ -172,7 +176,7 @@ class MTP_DEVICE(BASE):
steps = len(all_books) + 2 steps = len(all_books) + 2
count = 0 count = 0
self.report_progress(0, _('Reading metadata from device')) self.report_progress(0, _('Reading ebook metadata'))
# Read the cache if it exists # Read the cache if it exists
storage = self.filesystem_cache.storage(sid) storage = self.filesystem_cache.storage(sid)
cache = storage.find_path((self.METADATA_CACHE,)) cache = storage.find_path((self.METADATA_CACHE,))

View File

@ -239,10 +239,12 @@ class TestDeviceInteraction(unittest.TestCase):
# Test get_filesystem # Test get_filesystem
used_by_one = self.measure_memory_usage(1, 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, 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, self.check_memory(used_by_one, used_by_many,
'Memory consumption during get_filesystem') 'Memory consumption during get_filesystem')

View File

@ -212,6 +212,9 @@ class MTP_DEVICE(MTPDeviceBase):
ans += pprint.pformat(storage) ans += pprint.pformat(storage)
return ans return ans
def _filesystem_callback(self, entry):
self.filesystem_callback(_('Found object: %s')%entry.get('name', ''))
@property @property
def filesystem_cache(self): def filesystem_cache(self):
if self._filesystem_cache is None: if self._filesystem_cache is None:
@ -231,7 +234,8 @@ class MTP_DEVICE(MTPDeviceBase):
storage.append({'id':sid, 'size':capacity, storage.append({'id':sid, 'size':capacity,
'is_folder':True, 'name':name, 'can_delete':False, 'is_folder':True, 'name':name, 'can_delete':False,
'is_system':True}) '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) all_items.extend(items), all_errs.extend(errs)
if not all_items and all_errs: if not all_items and all_errs:
raise DeviceError( raise DeviceError(

View File

@ -357,7 +357,7 @@ Device_storage_info(Device *self, void *closure) {
// Device.get_filesystem {{{ // 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; LIBMTP_file_t *f, *files;
PyObject *entry; PyObject *entry;
int ok = 1; 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); entry = build_file_metadata(f, storage_id);
if (entry == NULL) { ok = 0; } if (entry == NULL) { ok = 0; }
else { else {
Py_XDECREF(PyObject_CallFunctionObjArgs(callback, entry, NULL));
if (PyList_Append(ans, entry) != 0) { ok = 0; } if (PyList_Append(ans, entry) != 0) { ok = 0; }
Py_DECREF(entry); Py_DECREF(entry);
} }
if (ok && f->filetype == LIBMTP_FILETYPE_FOLDER) { 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; ok = 0;
} }
} }
@ -394,19 +395,20 @@ static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uin
static PyObject * static PyObject *
Device_get_filesystem(Device *self, PyObject *args) { Device_get_filesystem(Device *self, PyObject *args) {
PyObject *ans, *errs; PyObject *ans, *errs, *callback;
unsigned long storage_id; unsigned long storage_id;
int ok = 0; int ok = 0;
ENSURE_DEV(NULL); ENSURE_STORAGE(NULL); 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); ans = PyList_New(0);
errs = PyList_New(0); errs = PyList_New(0);
if (errs == NULL || ans == NULL) { PyErr_NoMemory(); return NULL; } if (errs == NULL || ans == NULL) { PyErr_NoMemory(); return NULL; }
LIBMTP_Clear_Errorstack(self->device); 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); dump_errorstack(self->device, errs);
if (!ok) { if (!ok) {
Py_DECREF(ans); Py_DECREF(ans);
@ -535,7 +537,7 @@ static PyMethodDef Device_methods[] = {
}, },
{"get_filesystem", (PyCFunction)Device_get_filesystem, METH_VARARGS, {"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, {"get_file", (PyCFunction)Device_get_file, METH_VARARGS,

View File

@ -136,8 +136,9 @@ public:
HANDLE complete; HANDLE complete;
ULONG self_ref; ULONG self_ref;
PyThreadState *thread_state; 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() {} ~GetBulkCallback() {}
HRESULT __stdcall OnStart(REFGUID Context) { return S_OK; } HRESULT __stdcall OnStart(REFGUID Context) { return S_OK; }
@ -195,6 +196,7 @@ public:
Py_DECREF(temp); Py_DECREF(temp);
set_properties(obj, properties); set_properties(obj, properties);
Py_XDECREF(PyObject_CallFunctionObjArgs(callback, obj, NULL));
properties->Release(); properties = 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; PyObject *folders = NULL;
GUID guid_context = GUID_NULL; GUID guid_context = GUID_NULL;
HANDLE ev = NULL; HANDLE ev = NULL;
@ -227,7 +229,7 @@ static PyObject* bulk_get_filesystem(IPortableDevice *device, IPortableDevicePro
properties = create_filesystem_properties_collection(); properties = create_filesystem_properties_collection();
if (properties == NULL) goto end; 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; } if (callback == NULL) { PyErr_NoMemory(); goto end; }
hr = bulk_properties->QueueGetValuesByObjectList(object_ids, properties, callback, &guid_context); hr = bulk_properties->QueueGetValuesByObjectList(object_ids, properties, callback, &guid_context);
@ -272,7 +274,7 @@ end:
// }}} // }}}
// find_all_objects_in() {{{ // 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. * Find all children of the object identified by parent_id, recursively.
* The child ids are put into object_ids. Returns False if any errors * 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; DWORD fetched, i;
PROPVARIANT pv; PROPVARIANT pv;
BOOL ok = 1; BOOL ok = 1;
PyObject *id;
PropVariantInit(&pv); PropVariantInit(&pv);
pv.vt = VT_LPWSTR; pv.vt = VT_LPWSTR;
@ -303,10 +306,15 @@ static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevice
if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) {
for(i = 0; i < fetched; i++) { for(i = 0; i < fetched; i++) {
pv.pwszVal = child_ids[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); hr2 = object_ids->Add(&pv);
pv.pwszVal = NULL; pv.pwszVal = NULL;
if (FAILED(hr2)) { hresult_set_exc("Failed to add child ids to propvariantcollection", hr2); break; } 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; if (!ok) break;
} }
for (i = 0; i < fetched; i++) { CoTaskMemFree(child_ids[i]); child_ids[i] = NULL; } for (i = 0; i < fetched; i++) { CoTaskMemFree(child_ids[i]); child_ids[i] = NULL; }
@ -347,7 +355,7 @@ end:
return ans; 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; DWORD num, i;
PROPVARIANT pv; PROPVARIANT pv;
HRESULT hr; HRESULT hr;
@ -375,6 +383,7 @@ static PyObject* single_get_filesystem(IPortableDeviceContent *content, const wc
if (SUCCEEDED(hr) && pv.pwszVal != NULL) { if (SUCCEEDED(hr) && pv.pwszVal != NULL) {
item = get_object_properties(devprops, properties, pv.pwszVal); item = get_object_properties(devprops, properties, pv.pwszVal);
if (item != NULL) { if (item != NULL) {
Py_XDECREF(PyObject_CallFunctionObjArgs(callback, item, NULL));
PyDict_SetItem(ans, PyDict_GetItemString(item, "id"), item); PyDict_SetItem(ans, PyDict_GetItemString(item, "id"), item);
Py_DECREF(item); item = NULL; Py_DECREF(item); item = NULL;
ok = 1; ok = 1;
@ -429,7 +438,7 @@ end:
return values; 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; PyObject *folders = NULL;
IPortableDevicePropVariantCollection *object_ids = NULL; IPortableDevicePropVariantCollection *object_ids = NULL;
IPortableDeviceContent *content = NULL; IPortableDeviceContent *content = NULL;
@ -447,11 +456,11 @@ PyObject* wpd::get_filesystem(IPortableDevice *device, const wchar_t *storage_id
Py_END_ALLOW_THREADS; Py_END_ALLOW_THREADS;
if (FAILED(hr)) { hresult_set_exc("Failed to create propvariantcollection", hr); goto end; } 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 (!ok) goto end;
if (bulk_properties != NULL) folders = bulk_get_filesystem(device, bulk_properties, 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); else folders = single_get_filesystem(content, storage_id, object_ids, callback);
end: end:
if (content != NULL) content->Release(); if (content != NULL) content->Release();

View File

@ -78,14 +78,15 @@ update_data(Device *self, PyObject *args) {
// get_filesystem() {{{ // get_filesystem() {{{
static PyObject* static PyObject*
py_get_filesystem(Device *self, PyObject *args) { py_get_filesystem(Device *self, PyObject *args) {
PyObject *storage_id, *ret; PyObject *storage_id, *ret, *callback;
wchar_t *storage; 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); storage = unicode_to_wchar(storage_id);
if (storage == NULL) return NULL; 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); free(storage);
return ret; return ret;
} // }}} } // }}}
@ -163,7 +164,7 @@ static PyMethodDef Device_methods[] = {
}, },
{"get_filesystem", (PyCFunction)py_get_filesystem, METH_VARARGS, {"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, {"get_file", (PyCFunction)py_get_file, METH_VARARGS,

View File

@ -214,6 +214,14 @@ class MTP_DEVICE(MTPDeviceBase):
return True 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 @property
def filesystem_cache(self): def filesystem_cache(self):
if self._filesystem_cache is None: if self._filesystem_cache is None:
@ -233,7 +241,8 @@ class MTP_DEVICE(MTPDeviceBase):
break break
storage = {'id':storage_id, 'size':capacity, 'name':name, storage = {'id':storage_id, 'size':capacity, 'name':name,
'is_folder':True, 'can_delete':False, 'is_system':True} '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 for x in id_map.itervalues(): x['storage_id'] = storage_id
all_storage.append(storage) all_storage.append(storage)
items.append(id_map.itervalues()) items.append(id_map.itervalues())

View File

@ -56,7 +56,7 @@ int pump_waiting_messages();
extern IPortableDeviceValues* get_client_information(); extern IPortableDeviceValues* get_client_information();
extern IPortableDevice* open_device(const wchar_t *pnp_id, IPortableDeviceValues *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_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* 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* create_folder(IPortableDevice *device, const wchar_t *parent_id, const wchar_t *name);
extern PyObject* delete_object(IPortableDevice *device, const wchar_t *object_id); extern PyObject* delete_object(IPortableDevice *device, const wchar_t *object_id);

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re, tempfile, os import re, tempfile, os, imghdr
from functools import partial from functools import partial
from itertools import izip from itertools import izip
from urllib import quote from urllib import quote
@ -247,6 +247,15 @@ class HTMLInput(InputFormatPlugin):
if media_type == 'text/plain': if media_type == 'text/plain':
self.log.warn('Ignoring link to text file %r'%link_) self.log.warn('Ignoring link to text file %r'%link_)
return None 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.log.debug('Added', link)
self.oeb.container = self.DirContainer(os.path.dirname(link), self.oeb.container = self.DirContainer(os.path.dirname(link),

View File

@ -15,7 +15,6 @@ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation OptionRecommendation
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory
from calibre.constants import iswindows from calibre.constants import iswindows
from calibre import walk
UNITS = [ UNITS = [
'millimeter', 'millimeter',
@ -138,6 +137,85 @@ class PDFOutput(OutputFormatPlugin):
item = oeb.manifest.ids[cover_id] item = oeb.manifest.ids[cover_id]
self.cover_data = item.data 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): def convert_text(self, oeb_book):
from calibre.ebooks.pdf.writer import PDFWriter from calibre.ebooks.pdf.writer import PDFWriter
from calibre.ebooks.metadata.opf2 import OPF 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.log.debug('Serializing oeb input to disk for processing...')
self.get_cover_data() self.get_cover_data()
if iswindows:
self.remove_font_specification()
else:
self.handle_embedded_fonts()
with TemporaryDirectory('_pdf_out') as oeb_dir: with TemporaryDirectory('_pdf_out') as oeb_dir:
from calibre.customize.ui import plugin_for_output_format from calibre.customize.ui import plugin_for_output_format
oeb_output = plugin_for_output_format('oeb') oeb_output = plugin_for_output_format('oeb')
oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log) 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] opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0]
opf = OPF(opfpath, os.path.dirname(opfpath)) opf = OPF(opfpath, os.path.dirname(opfpath))

View File

@ -22,9 +22,10 @@ TEMPLATE = '''
li {{ list-style-type: none }} li {{ list-style-type: none }}
a {{ text-decoration: none }} a {{ text-decoration: none }}
a:hover {{ color: red }} a:hover {{ color: red }}
{extra_css}
</style> </style>
</head> </head>
<body> <body id="calibre_generated_inline_toc">
<h2>{title}</h2> <h2>{title}</h2>
<ul> <ul>
</ul> </ul>
@ -64,7 +65,7 @@ class TOCAdder(object):
self.log('\tGenerating in-line ToC') self.log('\tGenerating in-line ToC')
root = etree.fromstring(TEMPLATE.format(xhtmlns=XHTML_NS, 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 = XPath('//h:ul')(root)[0]
parent.text = '\n\t' parent.text = '\n\t'
for child in self.oeb.toc: for child in self.oeb.toc:

View File

@ -12,6 +12,7 @@ from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt,
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' APP_UID = 'libprs500'
from calibre import prints
from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx, from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx,
plugins, config_dir, filesystem_encoding, DEBUG) plugins, config_dir, filesystem_encoding, DEBUG)
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig 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.'+( path = os.path.join(sys.extensions_location, 'calibre_style.'+(
'pyd' if iswindows else 'so')) '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 # 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): for role in (orig_pal.Button, orig_pal.Window):
c = orig_pal.brush(role).color() c = orig_pal.brush(role).color()
@ -853,6 +855,8 @@ class Application(QApplication):
except: except:
import traceback import traceback
traceback.print_exc() 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'] != if force_calibre_style or (depth_ok and gprefs['ui_style'] !=
'system'): 'system'):

View File

@ -286,6 +286,14 @@ class DeleteAction(InterfaceAction):
current_row = view.row_count() - 1 current_row = view.row_count() - 1
view.set_current_row(current_row) 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): def delete_books(self, *args):
''' '''
Delete selected books from device or library. Delete selected books from device or library.
@ -325,16 +333,13 @@ class DeleteAction(InterfaceAction):
'removed from your calibre library. Are you sure?') 'removed from your calibre library. Are you sure?')
+'</p>', 'library_delete_books', self.gui): +'</p>', 'library_delete_books', self.gui):
return return
ci = view.currentIndex() next_id = view.next_id
row = None
if ci.isValid():
row = ci.row()
if len(rows) < 5: if len(rows) < 5:
view.model().delete_books_by_id(to_delete_ids) 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: else:
self.__md = MultiDeleter(self.gui, to_delete_ids, 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. # Device view is visible.
else: else:
if self.gui.stack.currentIndex() == 1: if self.gui.stack.currentIndex() == 1:

View File

@ -19,6 +19,7 @@ from calibre.ebooks.conversion.config import load_defaults, \
load_specifics, GuiRecommendations load_specifics, GuiRecommendations
from calibre import prepare_string_for_xml from calibre import prepare_string_for_xml
from calibre.customize.ui import plugin_for_input_format from calibre.customize.ui import plugin_for_input_format
from calibre.gui2.font_family_chooser import FontFamilyChooser
def config_widget_for_input_plugin(plugin): def config_widget_for_input_plugin(plugin):
name = plugin.name.lower().replace(' ', '_') name = plugin.name.lower().replace(' ', '_')
@ -144,6 +145,8 @@ class Widget(QWidget):
return ans return ans
elif isinstance(g, QFontComboBox): elif isinstance(g, QFontComboBox):
return unicode(QFontInfo(g.currentFont()).family()) return unicode(QFontInfo(g.currentFont()).family())
elif isinstance(g, FontFamilyChooser):
return g.font_family
elif isinstance(g, EncodingComboBox): elif isinstance(g, EncodingComboBox):
ans = unicode(g.currentText()).strip() ans = unicode(g.currentText()).strip()
try: try:
@ -208,6 +211,8 @@ class Widget(QWidget):
getattr(g, 'setCursorPosition', lambda x: x)(0) getattr(g, 'setCursorPosition', lambda x: x)(0)
elif isinstance(g, QFontComboBox): elif isinstance(g, QFontComboBox):
g.setCurrentFont(QFont(val or '')) g.setCurrentFont(QFont(val or ''))
elif isinstance(g, FontFamilyChooser):
g.font_family = val
elif isinstance(g, EncodingComboBox): elif isinstance(g, EncodingComboBox):
if val: if val:
g.setEditText(val) g.setEditText(val)

View File

@ -6,11 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import Qt
from calibre.gui2.convert.lrf_output_ui import Ui_Form from calibre.gui2.convert.lrf_output_ui import Ui_Form
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget
from calibre.gui2.widgets import FontFamilyModel
font_family_model = None font_family_model = None
@ -30,13 +27,6 @@ class PluginWidget(Widget, Ui_Form):
'header_separation', 'minimum_indent'] 'header_separation', 'minimum_indent']
) )
self.db, self.book_id = db, book_id 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.initialize_options(get_option, get_help, db, book_id)
self.opt_header.toggle(), self.opt_header.toggle() self.opt_header.toggle(), self.opt_header.toggle()
@ -44,14 +34,4 @@ class PluginWidget(Widget, Ui_Form):
self.opt_render_tables_as_images.toggle() 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

View File

@ -176,13 +176,13 @@
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QComboBox" name="opt_serif_family"/> <widget class="FontFamilyChooser" name="opt_serif_family"/>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QComboBox" name="opt_sans_family"/> <widget class="FontFamilyChooser" name="opt_sans_family"/>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="opt_mono_family"/> <widget class="FontFamilyChooser" name="opt_mono_family"/>
</item> </item>
</layout> </layout>
</widget> </widget>
@ -202,6 +202,13 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<customwidgets>
<customwidget>
<class>FontFamilyChooser</class>
<extends>QComboBox</extends>
<header>calibre/gui2/font_family_chooser.h</header>
</customwidget>
</customwidgets>
<resources/> <resources/>
<connections> <connections>
<connection> <connection>

View File

@ -101,8 +101,10 @@ class Sendmail(object):
from_ = 'calibre <calibre@'+socket.getfqdn()+'>' from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
with lopen(attachment, 'rb') as f: with lopen(attachment, 'rb') as f:
msg = compose_mail(from_, to, text, subject, f, aname) msg = compose_mail(from_, to, text, subject, f, aname)
efrom, eto = map(extract_email_address, (from_, to)) efrom = extract_email_address(from_)
eto = [eto] eto = []
for x in to.split(','):
eto.append(extract_email_address(x.strip()))
sendmail(msg, efrom, eto, localhost=None, sendmail(msg, efrom, eto, localhost=None,
verbose=1, verbose=1,
relay=opts.relay_host, relay=opts.relay_host,

View 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_()

View File

@ -867,6 +867,35 @@ class BooksView(QTableView): # {{{
break break
return property(fget=fget, fset=fset) 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): def close(self):
self._model.close() self._model.close()

View File

@ -9,10 +9,11 @@ __docformat__ = 'restructuredtext en'
import textwrap, re, os, errno, shutil import textwrap, re, os, errno, shutil
from PyQt4.Qt import (Qt, QDateTimeEdit, pyqtSignal, QMessageBox, from PyQt4.Qt import (Qt, QDateTimeEdit, pyqtSignal, QMessageBox, QIcon,
QIcon, QToolButton, QWidget, QLabel, QGridLayout, QApplication, QToolButton, QWidget, QLabel, QGridLayout, QApplication,
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog, QMenu, QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog, QMenu,
QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox, QAction) QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox,
QAction, QCalendarWidget, QDate)
from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView
from calibre.utils.icu import sort_key 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 = '' TOOLTIP = ''
LABEL = _('&Date:') LABEL = _('&Date:')
@ -1388,6 +1397,9 @@ class DateEdit(QDateTimeEdit): # {{{
fmt = self.FMT fmt = self.FMT
self.setDisplayFormat(fmt) self.setDisplayFormat(fmt)
self.setCalendarPopup(True) self.setCalendarPopup(True)
self.cw = CalendarWidget(self)
self.cw.setVerticalHeaderFormat(self.cw.NoVerticalHeader)
self.setCalendarWidget(self.cw)
self.setMinimumDateTime(UNDEFINED_QDATETIME) self.setMinimumDateTime(UNDEFINED_QDATETIME)
self.setSpecialValueText(_('Undefined')) self.setSpecialValueText(_('Undefined'))
self.clear_button = QToolButton(parent) self.clear_button = QToolButton(parent)

View File

@ -931,6 +931,7 @@ class FullFetch(QDialog): # {{{
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
l.addWidget(self.bb) l.addWidget(self.bb)
self.bb.rejected.connect(self.reject) 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 = self.bb.addButton(_('Next'), self.bb.AcceptRole)
self.next_button.setDefault(True) self.next_button.setDefault(True)
self.next_button.setEnabled(False) self.next_button.setEnabled(False)
@ -978,6 +979,7 @@ class FullFetch(QDialog): # {{{
self.log('\n\n') self.log('\n\n')
self.covers_widget.start(book, self.current_cover, self.covers_widget.start(book, self.current_cover,
self.title, self.authors, caches) self.title, self.authors, caches)
self.ok_button.setFocus()
def back_clicked(self): def back_clicked(self):
self.next_button.setVisible(True) self.next_button.setVisible(True)
@ -988,6 +990,8 @@ class FullFetch(QDialog): # {{{
self.covers_widget.reset_covers() self.covers_widget.reset_covers()
def accept(self): def accept(self):
if self.stack.currentIndex() == 1:
return QDialog.accept(self)
# Prevent the usual dialog accept mechanisms from working # Prevent the usual dialog accept mechanisms from working
pass pass

View File

@ -21,7 +21,7 @@ from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files,
info_dialog, error_dialog, open_url, available_height) info_dialog, error_dialog, open_url, available_height)
from calibre.ebooks.oeb.iterator.book import EbookIterator from calibre.ebooks.oeb.iterator.book import EbookIterator
from calibre.ebooks import DRMError 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.utils.config import Config, StringConfig, JSONConfig
from calibre.gui2.search_box import SearchBox2 from calibre.gui2.search_box import SearchBox2
from calibre.ebooks.metadata import MetaInformation 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.timeout.connect(self.viewport_resize_finished)
self.view_resized_timer.setSingleShot(True) self.view_resized_timer.setSingleShot(True)
self.resize_in_progress = False self.resize_in_progress = False
qs = [Qt.CTRL+Qt.Key_Q] qs = [Qt.CTRL+Qt.Key_Q,Qt.CTRL+Qt.Key_W]
if isosx:
qs += [Qt.CTRL+Qt.Key_W]
self.action_quit.setShortcuts(qs) self.action_quit.setShortcuts(qs)
self.action_quit.triggered.connect(self.quit) self.action_quit.triggered.connect(self.quit)
self.action_focus_search = QAction(self) self.action_focus_search = QAction(self)

View File

@ -85,7 +85,7 @@ class Kindle(Device):
output_profile = 'kindle' output_profile = 'kindle'
output_format = 'MOBI' output_format = 'MOBI'
name = 'Kindle 1-4 and Touch' name = 'Kindle Paperwhite/Touch/1-4'
manufacturer = 'Amazon' manufacturer = 'Amazon'
id = 'kindle' id = 'kindle'

View File

@ -3,12 +3,13 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Greg Riker' __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 copy import deepcopy
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
from calibre import (prepare_string_for_xml, strftime, force_unicode, from calibre import (prepare_string_for_xml, strftime, force_unicode,
isbytestring) isbytestring)
from calibre.constants import isosx
from calibre.customize.conversion import DummyReporter from calibre.customize.conversion import DummyReporter
from calibre.customize.ui import output_profiles from calibre.customize.ui import output_profiles
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
@ -643,6 +644,26 @@ class CatalogBuilder(object):
c = item c = item
ordnum, ordlen = collation_order(c) ordnum, ordlen = collation_order(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: if last_ordnum != ordnum:
last_c = icu_upper(c[0:ordlen]) last_c = icu_upper(c[0:ordlen])
if last_c in exceptions.keys(): if last_c in exceptions.keys():
@ -656,7 +677,7 @@ class CatalogBuilder(object):
for idx, item in enumerate(item_list): for idx, item in enumerate(item_list):
print(" %s %s" % (cl_list[idx],item[sort_field])) print(" %s %s" % (cl_list[idx],item[sort_field]))
else: else:
print(" %s %s" % (cl_list[0], item)) print(" %s %s" % (cl_list[idx], item))
return cl_list return cl_list

View File

@ -6,163 +6,68 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, sys from calibre.constants import iswindows
from calibre.constants import plugins, iswindows, islinux, isbsd class Fonts(object):
_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): def __init__(self):
Thread.__init__(self) if iswindows:
self.daemon = True from calibre.utils.fonts.win_fonts import load_winfonts
self.failed = False self.backend = load_winfonts()
else:
from calibre.utils.fonts.fc import fontconfig
self.backend = fontconfig
def run(self): def find_font_families(self, allowed_extensions={'ttf', 'otf'}):
config = None if iswindows:
if getattr(sys, 'frameworks_dir', False): return self.backend.font_families()
config_dir = os.path.join(os.path.dirname( return self.backend.find_font_families(allowed_extensions=allowed_extensions)
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): def files_for_family(self, family, normalize=True):
''' '''
Find all the variants in the font family `family`. 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`, 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 they are a tuple (slant, weight) otherwise they are strings from the set
`('normal', 'bold', 'italic', 'bi', 'light', 'li')` `('normal', 'bold', 'italic', 'bi', 'light', 'li')`
''' '''
self.wait() if iswindows:
if isinstance(family, unicode): from calibre.ptempfile import PersistentTemporaryFile
family = family.encode('utf-8') fonts = self.backend.fonts_for_family(family, normalize=normalize)
fonts = {} ans = {}
ofamily = str(family).decode('utf-8') for ft, val in fonts.iteritems():
for fullname, path, style, nfamily, weight, slant in \ ext, name, data = val
_fc.files_for_family(str(family)): pt = PersistentTemporaryFile('.'+ext)
style = (slant, weight) pt.write(data)
if normalize: pt.close()
italic = slant > 0 ans[ft] = (pt.name, name)
normal = weight == 80 return ans
bold = weight > 80 return self.backend.files_for_family(family, normalize=normalize)
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 fonts_for_family(self, family, normalize=True):
def match(self, name, all=False, verbose=False):
''' '''
Find the system font that most closely matches `name`, where `name` is a specification Just like files for family, except that it returns 3-tuples of the form
of the form:: (extension, full name, font data).
familyname-<pointsize>:<property1=value1>:<property2=value2>... '''
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` fontconfig = Fonts()
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(): def test():
from pprint import pprint; import os
pprint(fontconfig.find_font_families()) print(fontconfig.find_font_families())
pprint(fontconfig.files_for_family('liberation serif'))
m = 'times new roman' if iswindows else 'liberation serif' 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__': if __name__ == '__main__':
test() test()

View File

@ -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())

View 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()

View 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()

View 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 ()

View File

@ -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 :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 .. moduleauthor:: Kovid Goyal <kovid@kovidgoyal.net> Copyright 2009
*/ */
#define _UNICODE
#define UNICODE #define UNICODE
#define PY_SSIZE_T_CLEAN
#include <Windows.h> #include <Windows.h>
#include <strsafe.h> #include <Strsafe.h>
#include <vector> #include <Python.h>
#include <new>
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;
// 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) { static PyObject* wchar_to_unicode(const wchar_t *o) {
HDC hdc; PyObject *ans;
HFONT font; if (o == NULL) return NULL;
HFONT old_font = NULL; ans = PyUnicode_FromWideChar(o, wcslen(o));
UINT sz; if (ans == NULL) PyErr_NoMemory();
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);
return ans; 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) { // Enumerate font families {{{
for (i = 0; i < families->size(); i++) { struct EnumData {
if (lstrcmp(families->at(i), lpelfe->elfLogFont.lfFaceName) == 0) HDC hdc;
return 1; PyObject *families;
} };
tmp = new WCHAR[LF_FACESIZE];
swprintf_s(tmp, LF_FACESIZE, L"%s", lpelfe->elfLogFont.lfFaceName);
families->push_back(tmp); 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; return 1;
} }
static PyObject* enum_font_families(PyObject *self, PyObject *args) {
vector<LPWSTR>* find_font_families(void) {
LOGFONTW logfont; LOGFONTW logfont;
HDC hdc; 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)); SecureZeroMemory(&logfont, sizeof(logfont));
logfont.lfCharSet = DEFAULT_CHARSET; logfont.lfCharSet = DEFAULT_CHARSET;
logfont.lfPitchAndFamily = VARIABLE_PITCH | FF_DONTCARE; logfont.lfFaceName[0] = L'\0';
StringCchCopyW(logfont.lfFaceName, 2, L"\0");
hdc = GetDC(NULL); hdc = GetDC(NULL);
EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback, enum_data.hdc = hdc;
(LPARAM)(families), 0); enum_data.families = families;
EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback,
(LPARAM)(&enum_data), 0);
ReleaseDC(NULL, hdc); ReleaseDC(NULL, hdc);
return families; 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;
}
#ifdef TEST // 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;
int main(int argc, char **argv) { SecureZeroMemory(&lf, sizeof(lf));
vector<LPWSTR> *all_families;
size_t i;
all_families = find_font_families(); if (!PyArg_ParseTuple(args, "OOl", &pyname, &italic, &weight)) return NULL;
for (i = 0; i < all_families->size(); i++) family = unicode_to_wchar(pyname);
wprintf_s(L"%s\n", all_families->at(i)); if (family == NULL) { Py_DECREF(ans); return NULL; }
StringCchCopyW(lf.lfFaceName, LF_FACESIZE, family);
free(family);
free_families_vector(all_families); 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");
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); SelectObject(hdc, old_font);
} else PyErr_SetFromWindowsErr(0);
DeleteObject(hf);
} else PyErr_SetFromWindowsErr(0);
ReleaseDC(NULL, hdc); ReleaseDC(NULL, hdc);
if (data != NULL) printf("\nyay: %d\n", data->size());
delete data;
return 0; return ans;
} }
#else // }}}
#define PY_SSIZE_T_CLEAN static PyObject* add_font(PyObject *self, PyObject *args) {
#include <Python.h> 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);
}
static PyObject* add_system_font(PyObject *self, PyObject *args) {
PyObject *name;
LPWSTR path;
int num;
if (!PyArg_ParseTuple(args, "O", &name)) return NULL;
path = unicode_to_wchar(name);
if (path == NULL) return NULL;
num = AddFontResource(path);
if (num > 0)
SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
free(path);
return Py_BuildValue("i", num);
}
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 static
PyMethodDef fontconfig_methods[] = { PyMethodDef winfonts_methods[] = {
{"find_font_families", fontconfig_find_font_families, METH_VARARGS, {"enum_font_families", enum_font_families, METH_VARARGS,
"find_font_families(allowed_extensions)\n\n" "enum_font_families()\n\n"
"Find all font families on the system for fonts of the specified types. If no " "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)."
"types are specified all font families are returned."
}, },
{"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} {NULL, NULL, 0, NULL}
}; };
extern "C" {
PyMODINIT_FUNC PyMODINIT_FUNC
initfontconfig(void) { initwinfonts(void) {
PyObject *m; PyObject *m;
m = Py_InitModule3( m = Py_InitModule3(
"fontconfig", fontconfig_methods, "winfonts", winfonts_methods,
"Find fonts." "Windows font API"
); );
if (m == NULL) return; 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