diff --git a/manual/faq.rst b/manual/faq.rst index c71d440c8d..500b31b68f 100644 --- a/manual/faq.rst +++ b/manual/faq.rst @@ -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 diff --git a/recipes/foreign_policy.recipe b/recipes/foreign_policy.recipe index 0d6f9984fd..893d055a05 100644 --- a/recipes/foreign_policy.recipe +++ b/recipes/foreign_policy.recipe @@ -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 diff --git a/recipes/freenature.recipe b/recipes/freenature.recipe index 0b287842ec..34ac919f4e 100644 --- a/recipes/freenature.recipe +++ b/recipes/freenature.recipe @@ -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'

ADVERTISEMENT

', 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 diff --git a/recipes/icons/automatiseringgids.png b/recipes/icons/automatiseringgids.png new file mode 100644 index 0000000000..c042faa8d5 Binary files /dev/null and b/recipes/icons/automatiseringgids.png differ diff --git a/recipes/icons/fokkeensukke.png b/recipes/icons/fokkeensukke.png new file mode 100644 index 0000000000..a9c3b5f109 Binary files /dev/null and b/recipes/icons/fokkeensukke.png differ diff --git a/recipes/icons/tweakers_net.png b/recipes/icons/tweakers_net.png new file mode 100644 index 0000000000..b60be0f64c Binary files /dev/null and b/recipes/icons/tweakers_net.png differ diff --git a/recipes/icons/vrijnederland.png b/recipes/icons/vrijnederland.png new file mode 100644 index 0000000000..107951108f Binary files /dev/null and b/recipes/icons/vrijnederland.png differ diff --git a/recipes/twitchfilms.recipe b/recipes/twitchfilms.recipe index dab0643410..423dead311 100644 --- a/recipes/twitchfilms.recipe +++ b/recipes/twitchfilms.recipe @@ -1,5 +1,5 @@ __license__ = 'GPL v3' -__copyright__ = '2009-2011, Darko Miletic ' +__copyright__ = '2009-2012, Darko Miletic ' ''' 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']) diff --git a/recipes/wash_post.recipe b/recipes/wash_post.recipe index 61a469b47d..eff27b1f2a 100644 --- a/recipes/wash_post.recipe +++ b/recipes/wash_post.recipe @@ -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: diff --git a/resources/templates/fb2.xsl b/resources/templates/fb2.xsl index 060c90ebbe..84fdd323bd 100644 --- a/resources/templates/fb2.xsl +++ b/resources/templates/fb2.xsl @@ -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; } @@ -99,7 +101,7 @@ -
  • +
  • , # @@ -213,7 +215,7 @@ -
    +
    @@ -234,11 +236,11 @@ - + - + @@ -294,16 +296,30 @@ - - - - + + + + + + + + + + + + + + + + + +
    @@ -410,5 +426,13 @@ + + + + + + + + diff --git a/setup/extensions.py b/setup/extensions.py index f7d40ca72c..1827d32f4a 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -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: diff --git a/src/calibre/constants.py b/src/calibre/constants.py index dd7abd89f5..899157c13b 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -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: diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index 7821631e85..988df109fc 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -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') + diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 065dac9250..9931918a18 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -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'] diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 57bc8f6c6c..be2bab7638 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -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,)) diff --git a/src/calibre/devices/mtp/test.py b/src/calibre/devices/mtp/test.py index c273bac5e0..4eaf28a385 100644 --- a/src/calibre/devices/mtp/test.py +++ b/src/calibre/devices/mtp/test.py @@ -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') diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 71914cddc0..b8e8938c93 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -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( diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c index bf07c73a35..b62bd8a9c7 100644 --- a/src/calibre/devices/mtp/unix/libmtp.c +++ b/src/calibre/devices/mtp/unix/libmtp.c @@ -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, diff --git a/src/calibre/devices/mtp/windows/content_enumeration.cpp b/src/calibre/devices/mtp/windows/content_enumeration.cpp index 580f77f9b0..612ecbc915 100644 --- a/src/calibre/devices/mtp/windows/content_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/content_enumeration.cpp @@ -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(); diff --git a/src/calibre/devices/mtp/windows/device.cpp b/src/calibre/devices/mtp/windows/device.cpp index 3d8d442b6c..3886bb5e56 100644 --- a/src/calibre/devices/mtp/windows/device.cpp +++ b/src/calibre/devices/mtp/windows/device.cpp @@ -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, diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 202c8dfd6e..7253b4490c 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -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()) diff --git a/src/calibre/devices/mtp/windows/global.h b/src/calibre/devices/mtp/windows/global.h index 212afd2cec..2a9361c18b 100644 --- a/src/calibre/devices/mtp/windows/global.h +++ b/src/calibre/devices/mtp/windows/global.h @@ -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); diff --git a/src/calibre/ebooks/conversion/plugins/html_input.py b/src/calibre/ebooks/conversion/plugins/html_input.py index b0f897a9b5..f00ccb9d9b 100644 --- a/src/calibre/ebooks/conversion/plugins/html_input.py +++ b/src/calibre/ebooks/conversion/plugins/html_input.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __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), diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index b3eed763ac..da66a9be0d 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -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)) diff --git a/src/calibre/ebooks/mobi/writer8/toc.py b/src/calibre/ebooks/mobi/writer8/toc.py index 313c454535..a6a089b402 100644 --- a/src/calibre/ebooks/mobi/writer8/toc.py +++ b/src/calibre/ebooks/mobi/writer8/toc.py @@ -22,9 +22,10 @@ TEMPLATE = ''' li {{ list-style-type: none }} a {{ text-decoration: none }} a:hover {{ color: red }} + {extra_css} - +

    {title}

    @@ -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: diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 69cafebdef..0ac0783bd5 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -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'): diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 135591aa10..87bbc1928e 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -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?') +'

    ', '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: diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index 38fb641987..ac81816174 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -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) diff --git a/src/calibre/gui2/convert/lrf_output.py b/src/calibre/gui2/convert/lrf_output.py index 75764164dd..a643da6ed0 100644 --- a/src/calibre/gui2/convert/lrf_output.py +++ b/src/calibre/gui2/convert/lrf_output.py @@ -6,11 +6,8 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __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 + diff --git a/src/calibre/gui2/convert/lrf_output.ui b/src/calibre/gui2/convert/lrf_output.ui index ecbe673c61..753ec6110a 100644 --- a/src/calibre/gui2/convert/lrf_output.ui +++ b/src/calibre/gui2/convert/lrf_output.ui @@ -176,13 +176,13 @@ - + - + - + @@ -202,6 +202,13 @@ + + + FontFamilyChooser + QComboBox +
    calibre/gui2/font_family_chooser.h
    +
    +
    diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py index ece6d54e26..4ecf28e519 100644 --- a/src/calibre/gui2/email.py +++ b/src/calibre/gui2/email.py @@ -101,8 +101,10 @@ class Sendmail(object): from_ = 'calibre ' 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, diff --git a/src/calibre/gui2/font_family_chooser.py b/src/calibre/gui2/font_family_chooser.py new file mode 100644 index 0000000000..04d9dfdfb6 --- /dev/null +++ b/src/calibre/gui2/font_family_chooser.py @@ -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 ' +__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_() + diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index f8dc83273c..5ad6a2632f 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -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() diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 6e764e90d5..36605c7584 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -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) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index a59b7fb57a..11f1b4a339 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -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 diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index ffc5ae2ac7..35bbdcca22 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -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) diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py index d831307d9a..784b899464 100644 --- a/src/calibre/gui2/wizard/__init__.py +++ b/src/calibre/gui2/wizard/__init__.py @@ -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' diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 41c42b3705..baa28d5974 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -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 diff --git a/src/calibre/utils/fonts/__init__.py b/src/calibre/utils/fonts/__init__.py index 7b4f0abea4..a5563acd4e 100644 --- a/src/calibre/utils/fonts/__init__.py +++ b/src/calibre/utils/fonts/__init__.py @@ -6,163 +6,68 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __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-::... + 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() diff --git a/src/calibre/utils/fonts/embedflag.py b/src/calibre/utils/fonts/embedflag.py deleted file mode 100644 index 0c4e94bae6..0000000000 --- a/src/calibre/utils/fonts/embedflag.py +++ /dev/null @@ -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 ' -__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()) - diff --git a/src/calibre/utils/fonts/fc.py b/src/calibre/utils/fonts/fc.py new file mode 100644 index 0000000000..b6a4b1f906 --- /dev/null +++ b/src/calibre/utils/fonts/fc.py @@ -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 ' +__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-::... + + 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() diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py new file mode 100644 index 0000000000..f20f238481 --- /dev/null +++ b/src/calibre/utils/fonts/utils.py @@ -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 ' +__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() + diff --git a/src/calibre/utils/fonts/win_fonts.py b/src/calibre/utils/fonts/win_fonts.py new file mode 100644 index 0000000000..747580d45e --- /dev/null +++ b/src/calibre/utils/fonts/win_fonts.py @@ -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 ' +__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 () + diff --git a/src/calibre/utils/fonts/winfonts.cpp b/src/calibre/utils/fonts/winfonts.cpp index 8bd2cc7c02..f678d021c4 100644 --- a/src/calibre/utils/fonts/winfonts.cpp +++ b/src/calibre/utils/fonts/winfonts.cpp @@ -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 Copyright 2009 */ +#define _UNICODE #define UNICODE +#define PY_SSIZE_T_CLEAN #include -#include -#include - -using namespace std; - -vector *get_font_data(HDC hdc) { - DWORD sz; - vector *data; - sz = GetFontData(hdc, 0, 0, NULL, 0); - data = new vector(sz); - if (GetFontData(hdc, 0, 0, &((*data)[0]), sz) == GDI_ERROR) { - delete data; data = NULL; - } - return data; +#include +#include +#include +// 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 *families = (vector*)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(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* find_font_families(void) { +static PyObject* enum_font_families(PyObject *self, PyObject *args) { LOGFONTW logfont; HDC hdc; - vector *families; + PyObject *families; + struct EnumData enum_data; - families = new vector(); + 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 *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 *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 *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 -# +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