From f89dc2eb717658f4a59aa8150bd0414e94490f84 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Aug 2011 10:39:20 -0600 Subject: [PATCH 1/4] Add a new action 'Pick Random Book' that can be added to the toolbar via Preferences->Toolbars. Fixes #818315 ([Enhancement] Pick random book) --- imgsrc/random.svg | 758 +++++++++++++++++++++ resources/images/random.png | Bin 0 -> 4449 bytes src/calibre/customize/builtins.py | 8 +- src/calibre/gui2/actions/choose_library.py | 5 +- src/calibre/gui2/actions/random.py | 28 + src/calibre/gui2/actions/view.py | 2 +- 6 files changed, 795 insertions(+), 6 deletions(-) create mode 100644 imgsrc/random.svg create mode 100644 resources/images/random.png create mode 100644 src/calibre/gui2/actions/random.py diff --git a/imgsrc/random.svg b/imgsrc/random.svg new file mode 100644 index 0000000000..8dec21307e --- /dev/null +++ b/imgsrc/random.svg @@ -0,0 +1,758 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/resources/images/random.png b/resources/images/random.png new file mode 100644 index 0000000000000000000000000000000000000000..d8dfc3629e6160edf069b631a7181417a28db5f9 GIT binary patch literal 4449 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 literal 0 HcmV?d00001 diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 620254b1f5..ecf92195d5 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -843,6 +843,12 @@ class ActionNextMatch(InterfaceActionBase): description = _('Find the next or previous match when searching in ' 'your calibre library in highlight mode') +class ActionPickRandom(InterfaceActionBase): + name = 'Pick Random Book' + actual_plugin = 'calibre.gui2.actions.random:PickRandomAction' + description = _('Choose a random book from your calibre library') + + class ActionStore(InterfaceActionBase): name = 'Store' author = 'John Schember' @@ -873,7 +879,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore, - ActionPluginUpdater] + ActionPluginUpdater, ActionPickRandom] # }}} diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index b233575fa2..726fee7910 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -152,10 +152,7 @@ class ChooseLibraryAction(InterfaceAction): self.choose_menu.addMenu(self.maintenance_menu) def pick_random(self, *args): - import random - pick = random.randint(0, self.gui.library_view.model().rowCount(None)) - self.gui.library_view.set_current_row(pick) - self.gui.library_view.scroll_to_row(pick) + self.gui.iactions['Pick Random Book'].pick_random() def library_name(self): db = self.gui.library_view.model().db diff --git a/src/calibre/gui2/actions/random.py b/src/calibre/gui2/actions/random.py new file mode 100644 index 0000000000..6c722d16ae --- /dev/null +++ b/src/calibre/gui2/actions/random.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import random + +from calibre.gui2.actions import InterfaceAction + +class PickRandomAction(InterfaceAction): + + name = 'Pick Random Book' + action_spec = (_('Pick a random book'), 'random.png', 'Catalog builder', None) + dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device']) + + def genesis(self): + self.qaction.triggered.connect(self.pick_random) + + def pick_random(self): + pick = random.randint(0, self.gui.library_view.model().rowCount(None)) + self.gui.library_view.set_current_row(pick) + self.gui.library_view.scroll_to_row(pick) + + diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index a877a8f75f..f67b0767d6 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -207,7 +207,7 @@ class ViewAction(InterfaceAction): self._view_books([index]) def view_random(self, *args): - self.gui.iactions['Choose Library'].pick_random() + self.gui.iactions['Pick Random Book'].pick_random() self._view_books([self.gui.library_view.currentIndex()]) def _view_calibre_books(self, ids): From dc03a8eadf4598287903d5edf0c26f8ed493c029 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Aug 2011 11:20:47 -0600 Subject: [PATCH 2/4] MOBI Input: Speedup reading of HUFF/CDIC compressed files --- src/calibre/ebooks/mobi/debug.py | 2 +- src/calibre/ebooks/mobi/huffcdic.py | 166 +++++++++++++++------------- src/calibre/ebooks/mobi/reader.py | 11 +- 3 files changed, 98 insertions(+), 81 deletions(-) diff --git a/src/calibre/ebooks/mobi/debug.py b/src/calibre/ebooks/mobi/debug.py index 47450842d1..0c45a8078a 100644 --- a/src/calibre/ebooks/mobi/debug.py +++ b/src/calibre/ebooks/mobi/debug.py @@ -1361,7 +1361,7 @@ class MOBIFile(object): # {{{ huffrecs = [self.records[r].raw for r in self.huffman_record_nums] from calibre.ebooks.mobi.huffcdic import HuffReader huffs = HuffReader(huffrecs) - decompress = lambda x: huffs.decompress([x]) + decompress = huffs.unpack elif 'palmdoc' in self.mobi_header.compression.lower(): from calibre.ebooks.compression.palmdoc import decompress_doc decompress = decompress_doc diff --git a/src/calibre/ebooks/mobi/huffcdic.py b/src/calibre/ebooks/mobi/huffcdic.py index 693eb314d5..a8dc82cc27 100644 --- a/src/calibre/ebooks/mobi/huffcdic.py +++ b/src/calibre/ebooks/mobi/huffcdic.py @@ -1,8 +1,12 @@ -#!/usr/bin/env python +#!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) __license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + ''' Decompress MOBI files compressed with the Huff/cdic algorithm. Code thanks to darkninja and igorsk. @@ -12,82 +16,92 @@ import struct from calibre.ebooks.mobi import MobiError -class BitReader(object): - - def __init__(self, data): - self.data, self.pos, self.nbits = data + "\x00\x00\x00\x00", 0, len(data) * 8 - - def peek(self, n): - r, g = 0, 0 - while g < n: - r, g = (r << 8) | ord(self.data[(self.pos+g)>>3]), g + 8 - ((self.pos+g) & 7) - return (r >> (g - n)) & ((1 << n) - 1) - - def eat(self, n): - self.pos += n - return self.pos <= self.nbits - - def left(self): - return self.nbits - self.pos +class Reader(object): -class HuffReader(object): - - def __init__(self, huffs): - self.huffs = huffs - - if huffs[0][0:4] != 'HUFF' or huffs[0][4:8] != '\x00\x00\x00\x18': + def __init__(self): + self.q = struct.Struct(b'>Q').unpack_from + + def load_huff(self, huff): + if huff[0:8] != b'HUFF\x00\x00\x00\x18': raise MobiError('Invalid HUFF header') - - if huffs[1][0:4] != 'CDIC' or huffs[1][4:8] != '\x00\x00\x00\x10': - raise ValueError('Invalid CDIC header') - - self.entry_bits, = struct.unpack('>L', huffs[1][12:16]) - off1,off2 = struct.unpack('>LL', huffs[0][16:24]) - self.dict1 = struct.unpack('<256L', huffs[0][off1:off1+256*4]) - self.dict2 = struct.unpack('<64L', huffs[0][off2:off2+64*4]) - self.dicts = huffs[1:] - self.r = '' - - def _unpack(self, bits, depth = 0): - if depth > 32: - raise MobiError('Corrupt file') - - while bits.left(): - dw = bits.peek(32) - v = self.dict1[dw >> 24] - codelen = v & 0x1F + off1, off2 = struct.unpack_from(b'>LL', huff, 8) + + def dict1_unpack(v): + codelen, term, maxcode = v&0x1f, v&0x80, v>>8 assert codelen != 0 - code = dw >> (32 - codelen) - r = (v >> 8) - if not (v & 0x80): - while code < self.dict2[(codelen-1)*2]: - codelen += 1 - code = dw >> (32 - codelen) - r = self.dict2[(codelen-1)*2+1] - r -= code - assert codelen != 0 - if not bits.eat(codelen): - return - dicno = r >> self.entry_bits - off1 = 16 + (r - (dicno << self.entry_bits)) * 2 - dic = self.dicts[dicno] - off2 = 16 + ord(dic[off1]) * 256 + ord(dic[off1+1]) - blen = ord(dic[off2]) * 256 + ord(dic[off2+1]) - slice = dic[off2+2:off2+2+(blen&0x7fff)] - if blen & 0x8000: - self.r += slice - else: - self._unpack(BitReader(slice), depth + 1) + if codelen <= 8: + assert term + maxcode = ((maxcode + 1) << (32 - codelen)) - 1 + return (codelen, term, maxcode) + self.dict1 = map(dict1_unpack, struct.unpack_from(b'>256L', huff, off1)) + + dict2 = struct.unpack_from(b'>64L', huff, off2) + self.mincode, self.maxcode = (), () + for codelen, mincode in enumerate((0,) + dict2[0::2]): + self.mincode += (mincode << (32 - codelen), ) + for codelen, maxcode in enumerate((0,) + dict2[1::2]): + self.maxcode += (((maxcode + 1) << (32 - codelen)) - 1, ) + + self.dictionary = [] + + def load_cdic(self, cdic): + if cdic[0:8] != b'CDIC\x00\x00\x00\x10': + raise MobiError('Invalid CDIC header') + phrases, bits = struct.unpack_from(b'>LL', cdic, 8) + n = min(1<H').unpack_from + def getslice(off): + blen, = h(cdic, 16+off) + slice = cdic[18+off:18+off+(blen&0x7fff)] + return (slice, blen&0x8000) + self.dictionary += map(getslice, struct.unpack_from(b'>%dH' % n, cdic, 16)) def unpack(self, data): - self.r = '' - self._unpack(BitReader(data)) - return self.r - - def decompress(self, sections): - r = '' - for data in sections: - r += self.unpack(data) - if r.endswith('#'): - r = r[:-1] - return r + q = self.q + + bitsleft = len(data) * 8 + data += b'\x00\x00\x00\x00\x00\x00\x00\x00' + pos = 0 + x, = q(data, pos) + n = 32 + + s = [] + while True: + if n <= 0: + pos += 4 + x, = q(data, pos) + n += 32 + code = (x >> n) & ((1 << 32) - 1) + + codelen, term, maxcode = self.dict1[code >> 24] + if not term: + while code < self.mincode[codelen]: + codelen += 1 + maxcode = self.maxcode[codelen] + + n -= codelen + bitsleft -= codelen + if bitsleft < 0: + break + + r = (maxcode - code) >> (32 - codelen) + slice_, flag = self.dictionary[r] + if not flag: + self.dictionary[r] = None + slice_ = self.unpack(slice_) + self.dictionary[r] = (slice_, 1) + s.append(slice_) + return b''.join(s) + +class HuffReader(object): + + def __init__(self, huffs): + self.reader = Reader() + self.reader.load_huff(huffs[0]) + for cdic in huffs[1:]: + self.reader.load_cdic(cdic) + + def unpack(self, section): + return self.reader.unpack(section) + + diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index d704379cf1..dff09bc862 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -859,16 +859,19 @@ class MobiReader(object): processed_records += list(range(self.book_header.huff_offset, self.book_header.huff_offset + self.book_header.huff_number)) huff = HuffReader(huffs) - self.mobi_html = huff.decompress(text_sections) + unpack = huff.unpack elif self.book_header.compression_type == '\x00\x02': - for section in text_sections: - self.mobi_html += decompress_doc(section) + unpack = decompress_doc elif self.book_header.compression_type == '\x00\x01': - self.mobi_html = ''.join(text_sections) + unpack = lambda x: x else: raise MobiError('Unknown compression algorithm: %s' % repr(self.book_header.compression_type)) + self.mobi_html = b''.join(map(unpack, text_sections)) + if self.mobi_html.endswith(b'#'): + self.mobi_html = self.mobi_html[:-1] + if self.book_header.ancient and ' Date: Mon, 1 Aug 2011 11:43:48 -0600 Subject: [PATCH 3/4] ... --- src/calibre/gui2/book_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 802535b4e2..d7fb869400 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -167,7 +167,7 @@ def render_data(mi, use_roman_numbers=True, all_fields=False): elif metadata['datatype'] == 'datetime': aval = getattr(mi, field) if is_date_undefined(aval): - val = '' + continue ans.append((field, u'%s%s'%(name, val))) From b0b27b22533016ddaae5a9a18307eb92aee94153 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Aug 2011 21:39:09 -0600 Subject: [PATCH 4/4] ... --- src/calibre/ebooks/conversion/plumber.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index cee24521c7..7b39a3b6a5 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -557,7 +557,7 @@ OptionRecommendation(name='delete_blank_paragraphs', OptionRecommendation(name='format_scene_breaks', recommended_value=True, level=OptionRecommendation.LOW, help=_('Left aligned scene break markers are center aligned. ' - 'Replace soft scene breaks that use multiple blank lines with' + 'Replace soft scene breaks that use multiple blank lines with ' 'horizontal rules.')), OptionRecommendation(name='replace_scene_breaks',