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 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

View File

@ -6,40 +6,19 @@ www.foreignpolicy.com
from calibre.web.feeds.news import BasicNewsRecipe
class ForeignPolicy(BasicNewsRecipe):
title = 'Foreign Policy'
class AdvancedUserRecipe1349086293(BasicNewsRecipe):
title = u'Foreign Policy'
__author__ = 'Darko Miletic'
description = 'International News'
publisher = 'Washingtonpost.Newsweek Interactive, LLC'
category = 'news, politics, USA'
oldest_article = 31
oldest_article = 31
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'en'
remove_empty_feeds = True
extra_css = ' body{font-family: Georgia,"Times New Roman",Times,serif } img{margin-bottom: 0.4em} h1,h2,h3,h4,h5,h6{font-family: Arial,Helvetica,sans-serif} '
auto_cleanup = True
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [dict(attrs={'id':['art-mast','art-body','auth-bio']})]
remove_tags = [dict(name='iframe'),dict(attrs={'id':['share-box','base-ad']})]
remove_attributes = ['height','width']
feeds = [(u'Articles', u'http://www.foreignpolicy.com/node/feed')]
feeds = [(u'Foreign_Policy', u'http://www.foreignpolicy.com/node/feed')]
def print_version(self, url):
return url + '?print=yes&page=full'
return url + '?print=yes&hidecomments=yes&page=full'
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -11,23 +11,8 @@ class NatureNews(BasicNewsRecipe):
max_articles_per_feed = 50
no_stylesheets = True
keep_only_tags = [dict(name='div', attrs={'id':'content'})]
# remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'})
# remove_tags_after = dict(name='h2', attrs={'id':'comments'})
remove_tags = [
dict(name='h2', attrs={'id':'comments'}),
dict(attrs={'alt':'Advertisement'}),
dict(name='div', attrs={'class':'ad'}),
dict(attrs={'class':'Z3988'}),
dict(attrs={'class':['formatpublished','type-of-article','cleardiv','disclaimer','buttons','comments xoxo']}),
dict(name='a', attrs={'href':'#comments'}),
dict(name='h2',attrs={'class':'subheading plusicon icon-add-comment'})
]
preprocess_regexps = [
(re.compile(r'<p>ADVERTISEMENT</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
]
use_embedded_content = False
keep_only_tags = [dict(name='div', attrs={'id':'article'})]
extra_css = '''
.author { text-align: right; font-size: small; line-height:1em; margin-top:0px; margin-left:0; margin-right:0; margin-bottom: 0; }
.imagedescription { font-size: small; font-style:italic; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; }
@ -36,51 +21,3 @@ class NatureNews(BasicNewsRecipe):
feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')]
def preprocess_html(self,soup):
# The author name is slightly buried - dig it up
author = soup.find('p', {'class':'byline'})
if author:
# Find out the author's name
authornamediv = author.find('span',{'class':'author fn'})
authornamelink = authornamediv.find('a')
if authornamelink:
authorname = authornamelink.contents[0]
else:
authorname = authornamediv.contents[0]
# Stick the author's name in the byline tag
tag = Tag(soup,'div')
tag['class'] = 'author'
tag.insert(0,authorname.strip())
author.replaceWith(tag)
# Change the intro from a p to a div
intro = soup.find('p',{'class':'intro'})
if intro:
tag = Tag(soup,'div')
tag['class'] = 'intro'
tag.insert(0,intro.contents[0])
intro.replaceWith(tag)
# Change span class=imagedescription to div
descr = soup.find('span',{'class':'imagedescription'})
if descr:
tag = Tag(soup,'div')
tag['class'] = 'imagedescription'
tag.insert(0,descr.renderContents())
descr.replaceWith(tag)
# The references are in a list, let's make them simpler
reflistcont = soup.find('ul',{'id':'article-refrences'})
if reflistcont:
reflist = reflistcont.li.renderContents()
tag = Tag(soup,'div')
tag['class'] = 'article-references'
tag.insert(0,reflist)
reflistcont.replaceWith(tag)
# Within the id=content div, we need to remove all the stuff after the end of the class=entry-content
entrycontent = soup.find('div',{'class':'entry-content'})
for nextSibling in entrycontent.findNextSiblings():
nextSibling.extract()
return soup

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'
__copyright__ = '2009-2011, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2009-2012, Darko Miletic <darko.miletic at gmail.com>'
'''
twitchfilm.net/news/
'''
@ -15,7 +15,6 @@ class Twitchfilm(BasicNewsRecipe):
use_embedded_content = False
encoding = 'utf-8'
publisher = 'Twitch'
masthead_url = 'http://twitchfilm.com/img/logo.png'
category = 'twitch, twitchfilm, movie news, movie reviews, cult cinema, independent cinema, anime, foreign cinema, geek talk'
language = 'en'
@ -26,8 +25,8 @@ class Twitchfilm(BasicNewsRecipe):
, 'language' : language
}
keep_only_tags=[dict(attrs={'class':'asset-header'})]
remove_tags_after=dict(attrs={'class':'asset-body'})
keep_only_tags=[dict(attrs={'class':'entry'})]
remove_tags_after=dict(attrs={'class':'text'})
remove_tags = [ dict(name='div', attrs={'class':['social','categories']})
, dict(attrs={'id':'main-asset'})
, dict(name=['meta','link','iframe','embed','object'])

View File

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

View File

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

View File

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

View File

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

View File

@ -294,6 +294,8 @@ class KINDLE2(KINDLE):
PRODUCT_ID = [0x0002, 0x0004]
BCD = [0x0100]
# SUPPORTS_SUB_DIRS = False # Apparently the Paperwhite doesn't like files placed in subdirectories
# SUPPORTS_SUB_DIRS_FOR_SCAN = True
EXTRA_CUSTOMIZATION_MESSAGE = [
_('Send page number information when sending books') +

View File

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

View File

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

View File

@ -239,10 +239,12 @@ class TestDeviceInteraction(unittest.TestCase):
# Test get_filesystem
used_by_one = self.measure_memory_usage(1,
self.dev.dev.get_filesystem, self.storage.object_id)
self.dev.dev.get_filesystem, self.storage.object_id, lambda x:
x)
used_by_many = self.measure_memory_usage(5,
self.dev.dev.get_filesystem, self.storage.object_id)
self.dev.dev.get_filesystem, self.storage.object_id, lambda x:
x)
self.check_memory(used_by_one, used_by_many,
'Memory consumption during get_filesystem')

View File

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

View File

@ -357,7 +357,7 @@ Device_storage_info(Device *self, void *closure) {
// Device.get_filesystem {{{
static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uint32_t parent_id, PyObject *ans, PyObject *errs) {
static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uint32_t parent_id, PyObject *ans, PyObject *errs, PyObject *callback) {
LIBMTP_file_t *f, *files;
PyObject *entry;
int ok = 1;
@ -372,12 +372,13 @@ static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uin
entry = build_file_metadata(f, storage_id);
if (entry == NULL) { ok = 0; }
else {
Py_XDECREF(PyObject_CallFunctionObjArgs(callback, entry, NULL));
if (PyList_Append(ans, entry) != 0) { ok = 0; }
Py_DECREF(entry);
}
if (ok && f->filetype == LIBMTP_FILETYPE_FOLDER) {
if (!recursive_get_files(dev, storage_id, f->item_id, ans, errs)) {
if (!recursive_get_files(dev, storage_id, f->item_id, ans, errs, callback)) {
ok = 0;
}
}
@ -394,19 +395,20 @@ static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uin
static PyObject *
Device_get_filesystem(Device *self, PyObject *args) {
PyObject *ans, *errs;
PyObject *ans, *errs, *callback;
unsigned long storage_id;
int ok = 0;
ENSURE_DEV(NULL); ENSURE_STORAGE(NULL);
if (!PyArg_ParseTuple(args, "k", &storage_id)) return NULL;
if (!PyArg_ParseTuple(args, "kO", &storage_id, &callback)) return NULL;
if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback is not a callable"); return NULL; }
ans = PyList_New(0);
errs = PyList_New(0);
if (errs == NULL || ans == NULL) { PyErr_NoMemory(); return NULL; }
LIBMTP_Clear_Errorstack(self->device);
ok = recursive_get_files(self->device, (uint32_t)storage_id, 0, ans, errs);
ok = recursive_get_files(self->device, (uint32_t)storage_id, 0, ans, errs, callback);
dump_errorstack(self->device, errs);
if (!ok) {
Py_DECREF(ans);
@ -535,7 +537,7 @@ static PyMethodDef Device_methods[] = {
},
{"get_filesystem", (PyCFunction)Device_get_filesystem, METH_VARARGS,
"get_filesystem(storage_id) -> Get the list of files and folders on the device in storage_id. Returns files, errors."
"get_filesystem(storage_id, callback) -> Get the list of files and folders on the device in storage_id. Returns files, errors. callback must be a callable that accepts a single argument. It is called with every found object."
},
{"get_file", (PyCFunction)Device_get_file, METH_VARARGS,

View File

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

View File

@ -78,14 +78,15 @@ update_data(Device *self, PyObject *args) {
// get_filesystem() {{{
static PyObject*
py_get_filesystem(Device *self, PyObject *args) {
PyObject *storage_id, *ret;
PyObject *storage_id, *ret, *callback;
wchar_t *storage;
if (!PyArg_ParseTuple(args, "O", &storage_id)) return NULL;
if (!PyArg_ParseTuple(args, "OO", &storage_id, &callback)) return NULL;
if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback is not a callable"); return NULL; }
storage = unicode_to_wchar(storage_id);
if (storage == NULL) return NULL;
ret = wpd::get_filesystem(self->device, storage, self->bulk_properties);
ret = wpd::get_filesystem(self->device, storage, self->bulk_properties, callback);
free(storage);
return ret;
} // }}}
@ -163,7 +164,7 @@ static PyMethodDef Device_methods[] = {
},
{"get_filesystem", (PyCFunction)py_get_filesystem, METH_VARARGS,
"get_filesystem(storage_id) -> Get all files/folders on the storage identified by storage_id. Tries to use bulk operations when possible."
"get_filesystem(storage_id, callback) -> Get all files/folders on the storage identified by storage_id. Tries to use bulk operations when possible. callback must be a callable that accepts a single argument. It is called with every found id and then with the metadata for every id."
},
{"get_file", (PyCFunction)py_get_file, METH_VARARGS,

View File

@ -214,6 +214,14 @@ class MTP_DEVICE(MTPDeviceBase):
return True
def _filesystem_callback(self, obj):
if isinstance(obj, dict):
n = obj.get('name', '')
msg = _('Found object: %s')%n
else:
msg = _('Found id: %s')%obj
self.filesystem_callback(msg)
@property
def filesystem_cache(self):
if self._filesystem_cache is None:
@ -233,7 +241,8 @@ class MTP_DEVICE(MTPDeviceBase):
break
storage = {'id':storage_id, 'size':capacity, 'name':name,
'is_folder':True, 'can_delete':False, 'is_system':True}
id_map = self.dev.get_filesystem(storage_id)
id_map = self.dev.get_filesystem(storage_id,
self._filesystem_callback)
for x in id_map.itervalues(): x['storage_id'] = storage_id
all_storage.append(storage)
items.append(id_map.itervalues())

View File

@ -56,7 +56,7 @@ int pump_waiting_messages();
extern IPortableDeviceValues* get_client_information();
extern IPortableDevice* open_device(const wchar_t *pnp_id, IPortableDeviceValues *client_information);
extern PyObject* get_device_information(IPortableDevice *device, IPortableDevicePropertiesBulk **bulk_properties);
extern PyObject* get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties);
extern PyObject* get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties, PyObject *callback);
extern PyObject* get_file(IPortableDevice *device, const wchar_t *object_id, PyObject *dest, PyObject *callback);
extern PyObject* create_folder(IPortableDevice *device, const wchar_t *parent_id, const wchar_t *name);
extern PyObject* delete_object(IPortableDevice *device, const wchar_t *object_id);

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re, tempfile, os
import re, tempfile, os, imghdr
from functools import partial
from itertools import izip
from urllib import quote
@ -247,6 +247,15 @@ class HTMLInput(InputFormatPlugin):
if media_type == 'text/plain':
self.log.warn('Ignoring link to text file %r'%link_)
return None
if media_type == self.BINARY_MIME:
# Check for the common case, images
try:
img = imghdr.what(link)
except EnvironmentError:
pass
else:
if img:
media_type = self.guess_type('dummy.'+img)[0] or self.BINARY_MIME
self.oeb.log.debug('Added', link)
self.oeb.container = self.DirContainer(os.path.dirname(link),

View File

@ -15,7 +15,6 @@ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation
from calibre.ptempfile import TemporaryDirectory
from calibre.constants import iswindows
from calibre import walk
UNITS = [
'millimeter',
@ -138,6 +137,85 @@ class PDFOutput(OutputFormatPlugin):
item = oeb.manifest.ids[cover_id]
self.cover_data = item.data
def handle_embedded_fonts(self):
'''
Because of QtWebKit's inability to handle embedded fonts correctly, we
remove the embedded fonts and make them available system wide instead.
If you ever move to Qt WebKit 2.3+ then this will be unnecessary.
'''
from calibre.ebooks.oeb.base import urlnormalize
from calibre.gui2 import must_use_qt
from calibre.utils.fonts.utils import get_font_names, remove_embed_restriction
from PyQt4.Qt import QFontDatabase, QByteArray
# First find all @font-face rules and remove them, adding the embedded
# fonts to Qt
family_map = {}
for item in list(self.oeb.manifest):
if not hasattr(item.data, 'cssRules'): continue
remove = set()
for i, rule in enumerate(item.data.cssRules):
if rule.type == rule.FONT_FACE_RULE:
remove.add(i)
try:
s = rule.style
src = s.getProperty('src').propertyValue[0].uri
font_family = s.getProperty('font-family').propertyValue[0].value
except:
continue
path = item.abshref(src)
ff = self.oeb.manifest.hrefs.get(urlnormalize(path), None)
if ff is None:
continue
raw = ff.data
self.oeb.manifest.remove(ff)
try:
raw = remove_embed_restriction(raw)
except:
continue
must_use_qt()
QFontDatabase.addApplicationFontFromData(QByteArray(raw))
try:
family_name = get_font_names(raw)[0]
except:
family_name = None
if family_name:
family_map[icu_lower(font_family)] = family_name
for i in sorted(remove, reverse=True):
item.data.cssRules.pop(i)
# Now map the font family name specified in the css to the actual
# family name of the embedded font (they may be different in general).
for item in self.oeb.manifest:
if not hasattr(item.data, 'cssRules'): continue
for i, rule in enumerate(item.data.cssRules):
if rule.type != rule.STYLE_RULE: continue
ff = rule.style.getProperty('font-family')
if ff is None: continue
val = ff.propertyValue
for i in xrange(val.length):
k = icu_lower(val[i].value)
if k in family_map:
val[i].value = family_map[k]
def remove_font_specification(self):
# Qt produces image based pdfs on windows when non-generic fonts are specified
# This might change in Qt WebKit 2.3+ you will have to test.
for item in self.oeb.manifest:
if not hasattr(item.data, 'cssRules'): continue
for i, rule in enumerate(item.data.cssRules):
if rule.type != rule.STYLE_RULE: continue
ff = rule.style.getProperty('font-family')
if ff is None: continue
val = ff.propertyValue
for i in xrange(val.length):
k = icu_lower(val[i].value)
if k not in {'serif', 'sans', 'sans-serif', 'sansserif',
'monospace', 'cursive', 'fantasy'}:
val[i].value = ''
def convert_text(self, oeb_book):
from calibre.ebooks.pdf.writer import PDFWriter
from calibre.ebooks.metadata.opf2 import OPF
@ -145,21 +223,16 @@ class PDFOutput(OutputFormatPlugin):
self.log.debug('Serializing oeb input to disk for processing...')
self.get_cover_data()
if iswindows:
self.remove_font_specification()
else:
self.handle_embedded_fonts()
with TemporaryDirectory('_pdf_out') as oeb_dir:
from calibre.customize.ui import plugin_for_output_format
oeb_output = plugin_for_output_format('oeb')
oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log)
if iswindows:
# On windows Qt generates an image based PDF if the html uses
# embedded fonts. See https://launchpad.net/bugs/1053906
for f in walk(oeb_dir):
if f.rpartition('.')[-1].lower() in {'ttf', 'otf'}:
self.log.warn('Found embedded font %s, removing it, as '
'embedded fonts on windows are not supported by '
'the PDF Output plugin'%os.path.basename(f))
os.remove(f)
opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0]
opf = OPF(opfpath, os.path.dirname(opfpath))

View File

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

View File

@ -12,6 +12,7 @@ from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt,
ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500'
from calibre import prints
from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx,
plugins, config_dir, filesystem_encoding, DEBUG)
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
@ -796,7 +797,8 @@ class Application(QApplication):
path = os.path.join(sys.extensions_location, 'calibre_style.'+(
'pyd' if iswindows else 'so'))
self.pi.load_style(path, 'Calibre')
if not self.pi.load_style(path, 'Calibre'):
prints('Failed to load calibre style')
# On OSX, on some machines, colors can be invalid. See https://bugs.launchpad.net/bugs/1014900
for role in (orig_pal.Button, orig_pal.Window):
c = orig_pal.brush(role).color()
@ -853,6 +855,8 @@ class Application(QApplication):
except:
import traceback
traceback.print_exc()
if not depth_ok:
prints('Color depth is less than 32 bits disabling modern look')
if force_calibre_style or (depth_ok and gprefs['ui_style'] !=
'system'):

View File

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

View File

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

View File

@ -6,11 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import Qt
from calibre.gui2.convert.lrf_output_ui import Ui_Form
from calibre.gui2.convert import Widget
from calibre.gui2.widgets import FontFamilyModel
font_family_model = None
@ -30,13 +27,6 @@ class PluginWidget(Widget, Ui_Form):
'header_separation', 'minimum_indent']
)
self.db, self.book_id = db, book_id
global font_family_model
if font_family_model is None:
font_family_model = FontFamilyModel()
self.font_family_model = font_family_model
self.opt_serif_family.setModel(self.font_family_model)
self.opt_sans_family.setModel(self.font_family_model)
self.opt_mono_family.setModel(self.font_family_model)
self.initialize_options(get_option, get_help, db, book_id)
self.opt_header.toggle(), self.opt_header.toggle()
@ -44,14 +34,4 @@ class PluginWidget(Widget, Ui_Form):
self.opt_render_tables_as_images.toggle()
def set_value_handler(self, g, val):
if unicode(g.objectName()) in ('opt_serif_family',
'opt_sans_family', 'opt_mono_family'):
idx = -1
if val:
idx = g.findText(val, Qt.MatchFixedString)
if idx < 0:
idx = 0
g.setCurrentIndex(idx)
return True
return False

View File

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

View File

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

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
return property(fget=fget, fset=fset)
@property
def next_id(self):
'''
Return the id of the 'next' row (i.e. the first unselected row after
the current row).
'''
ci = self.currentIndex()
if not ci.isValid():
return None
selected_rows = frozenset([i.row() for i in self.selectedIndexes() if
i.isValid()])
column = ci.column()
for i in xrange(ci.row()+1, self.row_count()):
if i in selected_rows: continue
try:
return self.model().id(self.model().index(i, column))
except:
pass
# No unselected rows after the current row, look before
for i in xrange(ci.row()-1, -1, -1):
if i in selected_rows: continue
try:
return self.model().id(self.model().index(i, column))
except:
pass
return None
def close(self):
self._model.close()

View File

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

View File

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

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)
from calibre.ebooks.oeb.iterator.book import EbookIterator
from calibre.ebooks import DRMError
from calibre.constants import islinux, isbsd, isosx, filesystem_encoding
from calibre.constants import islinux, isbsd, filesystem_encoding
from calibre.utils.config import Config, StringConfig, JSONConfig
from calibre.gui2.search_box import SearchBox2
from calibre.ebooks.metadata import MetaInformation
@ -209,9 +209,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.view_resized_timer.timeout.connect(self.viewport_resize_finished)
self.view_resized_timer.setSingleShot(True)
self.resize_in_progress = False
qs = [Qt.CTRL+Qt.Key_Q]
if isosx:
qs += [Qt.CTRL+Qt.Key_W]
qs = [Qt.CTRL+Qt.Key_Q,Qt.CTRL+Qt.Key_W]
self.action_quit.setShortcuts(qs)
self.action_quit.triggered.connect(self.quit)
self.action_focus_search = QAction(self)

View File

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

View File

@ -3,12 +3,13 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Greg Riker'
import datetime, htmlentitydefs, os, re, shutil, unicodedata, zlib
import datetime, htmlentitydefs, os, platform, re, shutil, unicodedata, zlib
from copy import deepcopy
from xml.sax.saxutils import escape
from calibre import (prepare_string_for_xml, strftime, force_unicode,
isbytestring)
from calibre.constants import isosx
from calibre.customize.conversion import DummyReporter
from calibre.customize.ui import output_profiles
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
@ -643,12 +644,32 @@ class CatalogBuilder(object):
c = item
ordnum, ordlen = collation_order(c)
if last_ordnum != ordnum:
last_c = icu_upper(c[0:ordlen])
if last_c in exceptions.keys():
last_c = exceptions[unicode(last_c)]
last_ordnum = ordnum
cl_list[idx] = last_c
if isosx and platform.mac_ver()[0] < '10.7':
# Hackhackhackhackhack
# icu returns bogus results with curly apostrophes, maybe others under OS X 10.6.x
# When we see the magic combo of 0/-1 for ordnum/ordlen, special case the logic
if ordnum == 0 and ordlen == -1:
if icu_upper(c[0]) != last_c:
last_c = icu_upper(c[0])
if last_c in exceptions.keys():
last_c = exceptions[unicode(last_c)]
last_ordnum = ordnum
cl_list[idx] = last_c
else:
if last_ordnum != ordnum:
last_c = icu_upper(c[0:ordlen])
if last_c in exceptions.keys():
last_c = exceptions[unicode(last_c)]
last_ordnum = ordnum
cl_list[idx] = last_c
else:
if last_ordnum != ordnum:
last_c = icu_upper(c[0:ordlen])
if last_c in exceptions.keys():
last_c = exceptions[unicode(last_c)]
last_ordnum = ordnum
cl_list[idx] = last_c
if self.DEBUG and self.opts.verbose:
print(" establish_equivalencies():")
@ -656,7 +677,7 @@ class CatalogBuilder(object):
for idx, item in enumerate(item_list):
print(" %s %s" % (cl_list[idx],item[sort_field]))
else:
print(" %s %s" % (cl_list[0], item))
print(" %s %s" % (cl_list[idx], item))
return cl_list

View File

@ -6,163 +6,68 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, sys
from calibre.constants import iswindows
from calibre.constants import plugins, iswindows, islinux, isbsd
_fc, _fc_err = plugins['fontconfig']
if _fc is None:
raise RuntimeError('Failed to load fontconfig with error:'+_fc_err)
if islinux or isbsd:
Thread = object
else:
from threading import Thread
class FontConfig(Thread):
class Fonts(object):
def __init__(self):
Thread.__init__(self)
self.daemon = True
self.failed = False
if iswindows:
from calibre.utils.fonts.win_fonts import load_winfonts
self.backend = load_winfonts()
else:
from calibre.utils.fonts.fc import fontconfig
self.backend = fontconfig
def run(self):
config = None
if getattr(sys, 'frameworks_dir', False):
config_dir = os.path.join(os.path.dirname(
getattr(sys, 'frameworks_dir')), 'Resources', 'fonts')
if isinstance(config_dir, unicode):
config_dir = config_dir.encode(sys.getfilesystemencoding())
config = os.path.join(config_dir, 'fonts.conf')
if iswindows and getattr(sys, 'frozen', False):
config_dir = os.path.join(os.path.dirname(sys.executable),
'fontconfig')
if isinstance(config_dir, unicode):
config_dir = config_dir.encode(sys.getfilesystemencoding())
config = os.path.join(config_dir, 'fonts.conf')
try:
_fc.initialize(config)
except:
import traceback
traceback.print_exc()
self.failed = True
def wait(self):
if not (islinux or isbsd):
self.join()
if self.failed:
raise RuntimeError('Failed to initialize fontconfig')
def find_font_families(self, allowed_extensions=['ttf', 'otf']):
'''
Return an alphabetically sorted list of font families available on the system.
`allowed_extensions`: A list of allowed extensions for font file types. Defaults to
`['ttf', 'otf']`. If it is empty, it is ignored.
'''
self.wait()
ans = _fc.find_font_families([bytes('.'+x) for x in allowed_extensions])
ans = sorted(set(ans), cmp=lambda x,y:cmp(x.lower(), y.lower()))
ans2 = []
for x in ans:
try:
ans2.append(x.decode('utf-8'))
except UnicodeDecodeError:
continue
return ans2
def find_font_families(self, allowed_extensions={'ttf', 'otf'}):
if iswindows:
return self.backend.font_families()
return self.backend.find_font_families(allowed_extensions=allowed_extensions)
def files_for_family(self, family, normalize=True):
'''
Find all the variants in the font family `family`.
Returns a dictionary of tuples. Each tuple is of the form (Full font name, path to font file).
Returns a dictionary of tuples. Each tuple is of the form (path to font
file, Full font name).
The keys of the dictionary depend on `normalize`. If `normalize` is `False`,
they are a tuple (slant, weight) otherwise they are strings from the set
`('normal', 'bold', 'italic', 'bi', 'light', 'li')`
'''
self.wait()
if isinstance(family, unicode):
family = family.encode('utf-8')
fonts = {}
ofamily = str(family).decode('utf-8')
for fullname, path, style, nfamily, weight, slant in \
_fc.files_for_family(str(family)):
style = (slant, weight)
if normalize:
italic = slant > 0
normal = weight == 80
bold = weight > 80
if italic:
style = 'italic' if normal else 'bi' if bold else 'li'
else:
style = 'normal' if normal else 'bold' if bold else 'light'
try:
fullname, path = fullname.decode('utf-8'), path.decode('utf-8')
nfamily = nfamily.decode('utf-8')
except UnicodeDecodeError:
continue
if style in fonts:
if nfamily.lower().strip() == ofamily.lower().strip() \
and 'Condensed' not in fullname and 'ExtraLight' not in fullname:
fonts[style] = (path, fullname)
else:
fonts[style] = (path, fullname)
if iswindows:
from calibre.ptempfile import PersistentTemporaryFile
fonts = self.backend.fonts_for_family(family, normalize=normalize)
ans = {}
for ft, val in fonts.iteritems():
ext, name, data = val
pt = PersistentTemporaryFile('.'+ext)
pt.write(data)
pt.close()
ans[ft] = (pt.name, name)
return ans
return self.backend.files_for_family(family, normalize=normalize)
return fonts
def match(self, name, all=False, verbose=False):
def fonts_for_family(self, family, normalize=True):
'''
Find the system font that most closely matches `name`, where `name` is a specification
of the form::
familyname-<pointsize>:<property1=value1>:<property2=value2>...
Just like files for family, except that it returns 3-tuples of the form
(extension, full name, font data).
'''
if iswindows:
return self.backend.fonts_for_family(family, normalize=normalize)
files = self.backend.files_for_family(family, normalize=normalize)
ans = {}
for ft, val in files.iteritems():
name, f = val
ext = f.rpartition('.')[-1].lower()
ans[ft] = (ext, name, open(f, 'rb').read())
return ans
For example, `verdana:weight=bold:slant=italic`
Returns a list of dictionaries, or a single dictionary.
Each dictionary has the keys:
'weight', 'slant', 'family', 'file', 'fullname', 'style'
`all`: If `True` return a sorted list of matching fonts, where the sort
is in order of decreasing closeness of matching. If `False` only the
best match is returned. '''
self.wait()
if isinstance(name, unicode):
name = name.encode('utf-8')
fonts = []
for fullname, path, style, family, weight, slant in \
_fc.match(str(name), bool(all), bool(verbose)):
try:
fullname = fullname.decode('utf-8')
path = path.decode('utf-8')
style = style.decode('utf-8')
family = family.decode('utf-8')
fonts.append({
'fullname' : fullname,
'path' : path,
'style' : style,
'family' : family,
'weight' : weight,
'slant' : slant
})
except UnicodeDecodeError:
continue
return fonts if all else (fonts[0] if fonts else None)
fontconfig = FontConfig()
if islinux or isbsd:
# On X11 Qt also uses fontconfig, so initialization must happen in the
# main thread. In any case on X11 initializing fontconfig should be very
# fast
fontconfig.run()
else:
fontconfig.start()
fontconfig = Fonts()
def test():
from pprint import pprint;
pprint(fontconfig.find_font_families())
pprint(fontconfig.files_for_family('liberation serif'))
import os
print(fontconfig.find_font_families())
m = 'times new roman' if iswindows else 'liberation serif'
pprint(fontconfig.match(m+':slant=italic:weight=bold', verbose=True))
for ft, val in fontconfig.files_for_family(m).iteritems():
print val[0], ft, val[1], os.path.getsize(val[1])
if __name__ == '__main__':
test()

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
:synopsis: Pythonic interface to the fontconfig library
:synopsis: Pythonic interface to the windows font routines
.. moduleauthor:: Kovid Goyal <kovid@kovidgoyal.net> Copyright 2009
*/
#define _UNICODE
#define UNICODE
#define PY_SSIZE_T_CLEAN
#include <Windows.h>
#include <strsafe.h>
#include <vector>
using namespace std;
vector<BYTE> *get_font_data(HDC hdc) {
DWORD sz;
vector<BYTE> *data;
sz = GetFontData(hdc, 0, 0, NULL, 0);
data = new vector<BYTE>(sz);
if (GetFontData(hdc, 0, 0, &((*data)[0]), sz) == GDI_ERROR) {
delete data; data = NULL;
}
return data;
#include <Strsafe.h>
#include <Python.h>
#include <new>
// Utils {{{
static wchar_t* unicode_to_wchar(PyObject *o) {
wchar_t *buf;
Py_ssize_t len;
if (o == NULL) return NULL;
if (!PyUnicode_Check(o)) {PyErr_Format(PyExc_TypeError, "The python object must be a unicode object"); return NULL;}
len = PyUnicode_GET_SIZE(o);
buf = (wchar_t *)calloc(len+2, sizeof(wchar_t));
if (buf == NULL) { PyErr_NoMemory(); return NULL; }
len = PyUnicode_AsWideChar((PyUnicodeObject*)o, buf, len);
if (len == -1) { free(buf); PyErr_Format(PyExc_TypeError, "Invalid python unicode object."); return NULL; }
return buf;
}
BOOL is_font_embeddable(ENUMLOGFONTEX *lpelfe) {
HDC hdc;
HFONT font;
HFONT old_font = NULL;
UINT sz;
size_t i;
LPOUTLINETEXTMETRICW metrics;
BOOL ans = TRUE;
hdc = GetDC(NULL);
font = CreateFontIndirect(&lpelfe->elfLogFont);
if (font != NULL) {
old_font = SelectObject(hdc, font);
sz = GetOutlineTextMetrics(hdc, 0, NULL);
metrics = new OUTLINETEXTMETRICW[sz];
if ( GetOutlineTextMetrics(hdc, sz, metrics) != 0) {
for ( i = 0; i < sz; i++) {
if (metrics[i].otmfsType & 0x01) {
wprintf_s(L"Not embeddable: %s\n", lpelfe->elfLogFont.lfFaceName);
ans = FALSE; break;
}
}
} else ans = FALSE;
delete[] metrics;
DeleteObject(font);
SelectObject(hdc, old_font);
} else ans = FALSE;
ReleaseDC(NULL, hdc);
static PyObject* wchar_to_unicode(const wchar_t *o) {
PyObject *ans;
if (o == NULL) return NULL;
ans = PyUnicode_FromWideChar(o, wcslen(o));
if (ans == NULL) PyErr_NoMemory();
return ans;
}
int CALLBACK find_families_callback (
ENUMLOGFONTEX *lpelfe, /* pointer to logical-font data */
NEWTEXTMETRICEX *lpntme, /* pointer to physical-font data */
int FontType, /* type of font */
LPARAM lParam /* a combo box HWND */
) {
size_t i;
LPWSTR tmp;
vector<LPWSTR> *families = (vector<LPWSTR>*)lParam;
// }}}
if (FontType & TRUETYPE_FONTTYPE) {
for (i = 0; i < families->size(); i++) {
if (lstrcmp(families->at(i), lpelfe->elfLogFont.lfFaceName) == 0)
return 1;
}
tmp = new WCHAR[LF_FACESIZE];
swprintf_s(tmp, LF_FACESIZE, L"%s", lpelfe->elfLogFont.lfFaceName);
families->push_back(tmp);
}
// Enumerate font families {{{
struct EnumData {
HDC hdc;
PyObject *families;
};
static PyObject* logfont_to_dict(const ENUMLOGFONTEX *lf, const TEXTMETRIC *tm, DWORD font_type, HDC hdc) {
PyObject *name, *full_name, *style, *script;
LOGFONT f = lf->elfLogFont;
name = wchar_to_unicode(f.lfFaceName);
full_name = wchar_to_unicode(lf->elfFullName);
style = wchar_to_unicode(lf->elfStyle);
script = wchar_to_unicode(lf->elfScript);
return Py_BuildValue("{s:N, s:N, s:N, s:N, s:O, s:O, s:O, s:O, s:l}",
"name", name,
"full_name", full_name,
"style", style,
"script", script,
"is_truetype", (font_type & TRUETYPE_FONTTYPE) ? Py_True : Py_False,
"is_italic", (tm->tmItalic != 0) ? Py_True : Py_False,
"is_underlined", (tm->tmUnderlined != 0) ? Py_True : Py_False,
"is_strikeout", (tm->tmStruckOut != 0) ? Py_True : Py_False,
"weight", tm->tmWeight
);
}
static int CALLBACK find_families_callback(const ENUMLOGFONTEX *lpelfe, const TEXTMETRIC *lpntme, DWORD font_type, LPARAM lParam) {
struct EnumData *enum_data = reinterpret_cast<struct EnumData*>(lParam);
PyObject *font = logfont_to_dict(lpelfe, lpntme, font_type, enum_data->hdc);
if (font == NULL) return 0;
PyList_Append(enum_data->families, font);
return 1;
}
vector<LPWSTR>* find_font_families(void) {
static PyObject* enum_font_families(PyObject *self, PyObject *args) {
LOGFONTW logfont;
HDC hdc;
vector<LPWSTR> *families;
PyObject *families;
struct EnumData enum_data;
families = new vector<LPWSTR>();
families = PyList_New(0);
if (families == NULL) return PyErr_NoMemory();
SecureZeroMemory(&logfont, sizeof(logfont));
logfont.lfCharSet = DEFAULT_CHARSET;
logfont.lfPitchAndFamily = VARIABLE_PITCH | FF_DONTCARE;
StringCchCopyW(logfont.lfFaceName, 2, L"\0");
logfont.lfFaceName[0] = L'\0';
hdc = GetDC(NULL);
EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback,
(LPARAM)(families), 0);
enum_data.hdc = hdc;
enum_data.families = families;
EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback,
(LPARAM)(&enum_data), 0);
ReleaseDC(NULL, hdc);
return families;
}
inline void free_families_vector(vector<LPWSTR> *v) {
for (size_t i = 0; i < v->size(); i++) delete[] v->at(i);
delete v;
// }}}
// font_data() {{{
static PyObject* font_data(PyObject *self, PyObject *args) {
PyObject *ans = NULL, *italic, *pyname;
LOGFONTW lf;
HDC hdc;
LONG weight;
LPWSTR family = NULL;
HGDIOBJ old_font = NULL;
HFONT hf;
DWORD sz;
char *buf;
SecureZeroMemory(&lf, sizeof(lf));
if (!PyArg_ParseTuple(args, "OOl", &pyname, &italic, &weight)) return NULL;
family = unicode_to_wchar(pyname);
if (family == NULL) { Py_DECREF(ans); return NULL; }
StringCchCopyW(lf.lfFaceName, LF_FACESIZE, family);
free(family);
lf.lfItalic = (PyObject_IsTrue(italic)) ? 1 : 0;
lf.lfWeight = weight;
lf.lfOutPrecision = OUT_TT_ONLY_PRECIS;
hdc = GetDC(NULL);
if ( (hf = CreateFontIndirect(&lf)) != NULL) {
if ( (old_font = SelectObject(hdc, hf)) != NULL ) {
sz = GetFontData(hdc, 0, 0, NULL, 0);
if (sz != GDI_ERROR) {
buf = (char*)calloc(sz, sizeof(char));
if (buf != NULL) {
if (GetFontData(hdc, 0, 0, buf, sz) != GDI_ERROR) {
ans = PyBytes_FromStringAndSize(buf, sz);
if (ans == NULL) PyErr_NoMemory();
} else PyErr_SetString(PyExc_ValueError, "GDI Error");
free(buf);
} else PyErr_NoMemory();
} else PyErr_SetString(PyExc_ValueError, "GDI Error");
SelectObject(hdc, old_font);
} else PyErr_SetFromWindowsErr(0);
DeleteObject(hf);
} else PyErr_SetFromWindowsErr(0);
ReleaseDC(NULL, hdc);
return ans;
}
// }}}
static PyObject* add_font(PyObject *self, PyObject *args) {
char *data;
Py_ssize_t sz;
DWORD num = 0;
if (!PyArg_ParseTuple(args, "s#", &data, &sz)) return NULL;
AddFontMemResourceEx(data, sz, NULL, &num);
return Py_BuildValue("k", num);
}
#ifdef TEST
static PyObject* add_system_font(PyObject *self, PyObject *args) {
PyObject *name;
LPWSTR path;
int num;
int main(int argc, char **argv) {
vector<LPWSTR> *all_families;
size_t i;
if (!PyArg_ParseTuple(args, "O", &name)) return NULL;
path = unicode_to_wchar(name);
if (path == NULL) return NULL;
all_families = find_font_families();
for (i = 0; i < all_families->size(); i++)
wprintf_s(L"%s\n", all_families->at(i));
free_families_vector(all_families);
HDC hdc = GetDC(NULL);
HFONT font = CreateFont(72,0,0,0,0,0,0,0,0,0,0,0,0,L"Verdana");
HFONT old_font = SelectObject(hdc, font);
vector<BYTE> *data = get_font_data(hdc);
DeleteObject(font);
SelectObject(hdc, old_font);
ReleaseDC(NULL, hdc);
if (data != NULL) printf("\nyay: %d\n", data->size());
delete data;
return 0;
num = AddFontResource(path);
if (num > 0)
SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
free(path);
return Py_BuildValue("i", num);
}
#else
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#
static PyObject* remove_system_font(PyObject *self, PyObject *args) {
PyObject *name, *ok = Py_False;
LPWSTR path;
if (!PyArg_ParseTuple(args, "O", &name)) return NULL;
path = unicode_to_wchar(name);
if (path == NULL) return NULL;
if (RemoveFontResource(path)) {
SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0);
ok = Py_True;
}
free(path);
return Py_BuildValue("O", ok);
}
static
PyMethodDef fontconfig_methods[] = {
{"find_font_families", fontconfig_find_font_families, METH_VARARGS,
"find_font_families(allowed_extensions)\n\n"
"Find all font families on the system for fonts of the specified types. If no "
"types are specified all font families are returned."
PyMethodDef winfonts_methods[] = {
{"enum_font_families", enum_font_families, METH_VARARGS,
"enum_font_families()\n\n"
"Enumerate all regular (not italic/bold/etc. variants) font families on the system. Note there will be multiple entries for every family (corresponding to each charset of the font)."
},
{"font_data", font_data, METH_VARARGS,
"font_data(family_name, italic, weight)\n\n"
"Return the raw font data for the specified font."
},
{"add_font", add_font, METH_VARARGS,
"add_font(data)\n\n"
"Add the font(s) in the data (bytestring) to windows. Added fonts are always private. Returns the number of fonts added."
},
{"add_system_font", add_system_font, METH_VARARGS,
"add_system_font(data)\n\n"
"Add the font(s) in the specified file to the system font tables."
},
{"remove_system_font", remove_system_font, METH_VARARGS,
"remove_system_font(data)\n\n"
"Remove the font(s) in the specified file from the system font tables."
},
{NULL, NULL, 0, NULL}
};
extern "C" {
PyMODINIT_FUNC
initfontconfig(void) {
initwinfonts(void) {
PyObject *m;
m = Py_InitModule3(
"fontconfig", fontconfig_methods,
"Find fonts."
"winfonts", winfonts_methods,
"Windows font API"
);
if (m == NULL) return;
}
PyModule_AddIntMacro(m, FW_DONTCARE);
PyModule_AddIntMacro(m, FW_THIN);
PyModule_AddIntMacro(m, FW_EXTRALIGHT);
PyModule_AddIntMacro(m, FW_ULTRALIGHT);
PyModule_AddIntMacro(m, FW_LIGHT);
PyModule_AddIntMacro(m, FW_NORMAL);
PyModule_AddIntMacro(m, FW_REGULAR);
PyModule_AddIntMacro(m, FW_MEDIUM);
PyModule_AddIntMacro(m, FW_SEMIBOLD);
PyModule_AddIntMacro(m, FW_DEMIBOLD);
PyModule_AddIntMacro(m, FW_BOLD);
PyModule_AddIntMacro(m, FW_EXTRABOLD);
PyModule_AddIntMacro(m, FW_ULTRABOLD);
PyModule_AddIntMacro(m, FW_HEAVY);
PyModule_AddIntMacro(m, FW_BLACK);
}
#endif