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 @@
+
+
+
+
diff --git a/resources/images/random.png b/resources/images/random.png
new file mode 100644
index 0000000000..d8dfc3629e
Binary files /dev/null and b/resources/images/random.png differ
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/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',
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 ''
+__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):
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)))