home *** CD-ROM | disk | FTP | other *** search
/ Maximum CD 2010 November / maximum-cd-2010-11.iso / DiscContents / calibre-0.7.13.msi / file_986 (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2010-08-06  |  29.1 KB  |  928 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.6)
  3.  
  4. __license__ = 'GPL v3'
  5. __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
  6. import shutil
  7. import os
  8. import re
  9. import struct
  10. import textwrap
  11. import cStringIO
  12. import sys
  13.  
  14. try:
  15.     from PIL import Image as PILImage
  16.     PILImage
  17. except ImportError:
  18.     import Image as PILImage
  19.  
  20. from lxml import html, etree
  21. from calibre import xml_entity_to_unicode, CurrentDir, entity_to_unicode, replace_entities
  22. from calibre.utils.filenames import ascii_filename
  23. from calibre.utils.date import parse_date
  24. from calibre.ptempfile import TemporaryDirectory
  25. from calibre.ebooks import DRMError
  26. from calibre.ebooks.chardet import ENCODING_PATS
  27. from calibre.ebooks.mobi import MobiError
  28. from calibre.ebooks.mobi.huffcdic import HuffReader
  29. from calibre.ebooks.mobi.langcodes import main_language, sub_language, mobi2iana
  30. from calibre.ebooks.compression.palmdoc import decompress_doc
  31. from calibre.ebooks.metadata import MetaInformation
  32. from calibre.ebooks.metadata.opf2 import OPFCreator, OPF
  33. from calibre.ebooks.metadata.toc import TOC
  34.  
  35. class EXTHHeader(object):
  36.     
  37.     def __init__(self, raw, codec, title):
  38.         self.doctype = raw[:4]
  39.         (self.length, self.num_items) = struct.unpack('>LL', raw[4:12])
  40.         raw = raw[12:]
  41.         pos = 0
  42.         self.mi = MetaInformation(_('Unknown'), [
  43.             _('Unknown')])
  44.         self.has_fake_cover = True
  45.         left = self.num_items
  46.         while left > 0:
  47.             left -= 1
  48.             (id, size) = struct.unpack('>LL', raw[pos:pos + 8])
  49.             content = raw[pos + 8:pos + size]
  50.             pos += size
  51.             if id >= 100 and id < 200:
  52.                 self.process_metadata(id, content, codec)
  53.                 continue
  54.             if id == 203:
  55.                 self.has_fake_cover = bool(struct.unpack('>L', content)[0])
  56.                 continue
  57.             if id == 201:
  58.                 (co,) = struct.unpack('>L', content)
  59.                 if co < 1e+07:
  60.                     self.cover_offset = co
  61.                 
  62.             co < 1e+07
  63.             if id == 202:
  64.                 (self.thumbnail_offset,) = struct.unpack('>L', content)
  65.                 continue
  66.             if id == 501:
  67.                 continue
  68.             if id == 502:
  69.                 continue
  70.             if id == 503:
  71.                 if not title and title == _('Unknown') and 'USER_CONTENT' in title or title.startswith('dtp_'):
  72.                     
  73.                     try:
  74.                         title = content.decode(codec)
  75.  
  76.                 
  77.             title.startswith('dtp_')
  78.         if title:
  79.             self.mi.title = replace_entities(title)
  80.         
  81.  
  82.     
  83.     def process_metadata(self, id, content, codec):
  84.         if id == 100:
  85.             if self.mi.authors == [
  86.                 _('Unknown')]:
  87.                 self.mi.authors = []
  88.             
  89.             au = content.decode(codec, 'ignore').strip()
  90.             self.mi.authors.append(au)
  91.             if re.match('\\S+?\\s*,\\s+\\S+', au.strip()):
  92.                 self.mi.author_sort = au.strip()
  93.             
  94.         elif id == 101:
  95.             self.mi.publisher = content.decode(codec, 'ignore').strip()
  96.         elif id == 103:
  97.             self.mi.comments = content.decode(codec, 'ignore')
  98.         elif id == 104:
  99.             self.mi.isbn = content.decode(codec, 'ignore').strip().replace('-', '')
  100.         elif id == 105:
  101.             if not self.mi.tags:
  102.                 self.mi.tags = []
  103.             
  104.             []([ x.strip() for x in content.decode(codec, 'ignore').split(';') ])
  105.             self.mi.tags = list(set(self.mi.tags))
  106.         elif id == 106:
  107.             
  108.             try:
  109.                 self.mi.pubdate = parse_date(content, as_utc = False)
  110.  
  111.         elif id == 108:
  112.             pass
  113.         
  114.  
  115.  
  116.  
  117. class BookHeader(object):
  118.     
  119.     def __init__(self, raw, ident, user_encoding, log, try_extra_data_fix = False):
  120.         self.log = log
  121.         self.compression_type = raw[:2]
  122.         (self.records, self.records_size) = struct.unpack('>HH', raw[8:12])
  123.         (self.encryption_type,) = struct.unpack('>H', raw[12:14])
  124.         if ident == 'TEXTREAD':
  125.             self.codepage = 1252
  126.         
  127.         if len(raw) <= 16:
  128.             self.codec = 'cp1252'
  129.             self.extra_flags = 0
  130.             self.title = _('Unknown')
  131.             self.language = 'ENGLISH'
  132.             self.sublanguage = 'NEUTRAL'
  133.             (self.exth_flag, self.exth) = (0, None)
  134.             self.ancient = True
  135.             self.first_image_index = -1
  136.             self.mobi_version = 1
  137.         else:
  138.             self.ancient = False
  139.             self.doctype = raw[16:20]
  140.             (self.length, self.type, self.codepage, self.unique_id, self.version) = struct.unpack('>LLLLL', raw[20:40])
  141.             
  142.             try:
  143.                 self.codec = {
  144.                     1252: 'cp1252',
  145.                     65001: 'utf-8' }[self.codepage]
  146.             except (IndexError, KeyError):
  147.                 self.codec = None if user_encoding is None else user_encoding
  148.                 log.warn('Unknown codepage %d. Assuming %s' % (self.codepage, self.codec))
  149.  
  150.             if (ident == 'TEXTREAD' and self.length < 228 and 232 < self.length or try_extra_data_fix) and self.length == 228:
  151.                 self.extra_flags = 0
  152.             else:
  153.                 (self.extra_flags,) = struct.unpack('>H', raw[242:244])
  154.             if self.compression_type == 'DH':
  155.                 (self.huff_offset, self.huff_number) = struct.unpack('>LL', raw[112:120])
  156.             
  157.             (toff, tlen) = struct.unpack('>II', raw[84:92])
  158.             tend = toff + tlen
  159.             self.title = None if tend < len(raw) else _('Unknown')
  160.             langcode = struct.unpack('!L', raw[92:96])[0]
  161.             langid = langcode & 255
  162.             sublangid = langcode >> 10 & 255
  163.             self.language = main_language.get(langid, 'ENGLISH')
  164.             self.sublanguage = sub_language.get(sublangid, 'NEUTRAL')
  165.             self.mobi_version = struct.unpack('>I', raw[104:108])[0]
  166.             self.first_image_index = struct.unpack('>L', raw[108:112])[0]
  167.             (self.exth_flag,) = struct.unpack('>L', raw[128:132])
  168.             self.exth = None
  169.             if not isinstance(self.title, unicode):
  170.                 self.title = self.title.decode(self.codec, 'replace')
  171.             
  172.             if self.exth_flag & 64:
  173.                 
  174.                 try:
  175.                     self.exth = EXTHHeader(raw[16 + self.length:], self.codec, self.title)
  176.                     self.exth.mi.uid = self.unique_id
  177.                     
  178.                     try:
  179.                         self.exth.mi.language = mobi2iana(langid, sublangid)
  180.                     except:
  181.                         self.log.exception('Unknown language code')
  182.  
  183.                 self.log.exception('Invalid EXTH header')
  184.                 self.exth_flag = 0
  185.  
  186.             
  187.  
  188.  
  189.  
  190. class MetadataHeader(BookHeader):
  191.     
  192.     def __init__(self, stream, log):
  193.         self.stream = stream
  194.         self.ident = self.identity()
  195.         self.num_sections = self.section_count()
  196.         if self.num_sections >= 2:
  197.             header = self.header()
  198.             BookHeader.__init__(self, header, self.ident, None, log)
  199.         else:
  200.             self.exth = None
  201.  
  202.     
  203.     def identity(self):
  204.         self.stream.seek(60)
  205.         ident = self.stream.read(8).upper()
  206.         if ident not in ('BOOKMOBI', 'TEXTREAD'):
  207.             raise MobiError('Unknown book type: %s' % ident)
  208.         ident not in ('BOOKMOBI', 'TEXTREAD')
  209.         return ident
  210.  
  211.     
  212.     def section_count(self):
  213.         self.stream.seek(76)
  214.         return struct.unpack('>H', self.stream.read(2))[0]
  215.  
  216.     
  217.     def section_offset(self, number):
  218.         self.stream.seek(78 + number * 8)
  219.         return struct.unpack('>LBBBB', self.stream.read(8))[0]
  220.  
  221.     
  222.     def header(self):
  223.         section_headers = []
  224.         section_headers.append(self.section_offset(0))
  225.         section_headers.append(self.section_offset(1))
  226.         end_off = section_headers[1]
  227.         off = section_headers[0]
  228.         self.stream.seek(off)
  229.         return self.stream.read(end_off - off)
  230.  
  231.     
  232.     def section_data(self, number):
  233.         start = self.section_offset(number)
  234.         if number == self.num_sections - 1:
  235.             end = os.stat(self.stream.name).st_size
  236.         else:
  237.             end = self.section_offset(number + 1)
  238.         self.stream.seek(start)
  239.         return self.stream.read(end - start)
  240.  
  241.  
  242.  
  243. class MobiReader(object):
  244.     PAGE_BREAK_PAT = re.compile('(<[/]{0,1}mbp:pagebreak\\s*[/]{0,1}>)+', re.IGNORECASE)
  245.     IMAGE_ATTRS = ('lowrecindex', 'recindex', 'hirecindex')
  246.     
  247.     def __init__(self, filename_or_stream, log, user_encoding = None, debug = None, try_extra_data_fix = False):
  248.         self.log = log
  249.         self.debug = debug
  250.         self.embedded_mi = None
  251.         self.base_css_rules = textwrap.dedent('\n                blockquote { margin: 0em 0em 0em 1.25em; text-align: justify }\n\n                p { margin: 0em; text-align: justify }\n\n                .bold { font-weight: bold }\n\n                .italic { font-style: italic }\n\n                .mbp_pagebreak {\n                    page-break-after: always; margin: 0; display: block\n                }\n                ')
  252.         self.tag_css_rules = { }
  253.         if hasattr(filename_or_stream, 'read'):
  254.             stream = filename_or_stream
  255.             stream.seek(0)
  256.         else:
  257.             stream = open(filename_or_stream, 'rb')
  258.         raw = stream.read()
  259.         if raw.startswith('TPZ'):
  260.             raise ValueError(_('This is an Amazon Topaz book. It cannot be processed.'))
  261.         raw.startswith('TPZ')
  262.         self.header = raw[0:72]
  263.         self.name = self.header[:32].replace('\x00', '')
  264.         (self.num_sections,) = struct.unpack('>H', raw[76:78])
  265.         self.ident = self.header[60:68].upper()
  266.         if self.ident not in ('BOOKMOBI', 'TEXTREAD'):
  267.             raise MobiError('Unknown book type: %s' % repr(self.ident))
  268.         self.ident not in ('BOOKMOBI', 'TEXTREAD')
  269.         self.sections = []
  270.         self.section_headers = []
  271.         for i in range(self.num_sections):
  272.             (offset, a1, a2, a3, a4) = struct.unpack('>LBBBB', raw[78 + i * 8:78 + i * 8 + 8])
  273.             flags = a1
  274.             val = a2 << 16 | a3 << 8 | a4
  275.             self.section_headers.append((offset, flags, val))
  276.         
  277.         
  278.         def section(section_number):
  279.             if section_number == self.num_sections - 1:
  280.                 end_off = len(raw)
  281.             else:
  282.                 end_off = self.section_headers[section_number + 1][0]
  283.             off = self.section_headers[section_number][0]
  284.             return raw[off:end_off]
  285.  
  286.         for i in range(self.num_sections):
  287.             self.sections.append((section(i), self.section_headers[i]))
  288.         
  289.         self.book_header = BookHeader(self.sections[0][0], self.ident, user_encoding, self.log, try_extra_data_fix = try_extra_data_fix)
  290.         self.name = self.name.decode(self.book_header.codec, 'replace')
  291.  
  292.     
  293.     def extract_content(self, output_dir, parse_cache):
  294.         output_dir = os.path.abspath(output_dir)
  295.         if self.book_header.encryption_type != 0:
  296.             raise DRMError(self.name)
  297.         self.book_header.encryption_type != 0
  298.         processed_records = self.extract_text()
  299.         if self.debug is not None:
  300.             parse_cache['calibre_raw_mobi_markup'] = self.mobi_html
  301.         
  302.         self.add_anchors()
  303.         self.processed_html = self.processed_html.decode(self.book_header.codec, 'ignore')
  304.         self.processed_html = self.processed_html.replace('</</', '</')
  305.         self.processed_html = re.sub('</([a-zA-Z]+)<', '</\\1><', self.processed_html)
  306.         for pat in ENCODING_PATS:
  307.             self.processed_html = pat.sub('', self.processed_html)
  308.         
  309.         self.processed_html = re.sub('&(\\S+?);', xml_entity_to_unicode, self.processed_html)
  310.         self.extract_images(processed_records, output_dir)
  311.         self.replace_page_breaks()
  312.         self.cleanup_html()
  313.         self.log.debug('Parsing HTML...')
  314.         
  315.         try:
  316.             root = html.fromstring(self.processed_html)
  317.             if len(root.xpath('//html')) > 5:
  318.                 root = html.fromstring(self.processed_html.replace('\x0c', '').replace('\x14', ''))
  319.         except:
  320.             self.log.warning('MOBI markup appears to contain random bytes. Stripping.')
  321.             self.processed_html = self.remove_random_bytes(self.processed_html)
  322.             root = html.fromstring(self.processed_html)
  323.  
  324.         if root.xpath('descendant::p/descendant::p'):
  325.             soupparser = soupparser
  326.             import lxml.html
  327.             self.log.warning('Malformed markup, parsing using BeautifulSoup')
  328.             
  329.             try:
  330.                 root = soupparser.fromstring(self.processed_html)
  331.             except Exception:
  332.                 self.log.warning('MOBI markup appears to contain random bytes. Stripping.')
  333.                 self.processed_html = self.remove_random_bytes(self.processed_html)
  334.                 root = soupparser.fromstring(self.processed_html)
  335.             except:
  336.                 None<EXCEPTION MATCH>Exception
  337.             
  338.  
  339.         None<EXCEPTION MATCH>Exception
  340.         if root.tag != 'html':
  341.             self.log.warn('File does not have opening <html> tag')
  342.             nroot = html.fromstring('<html><head></head><body></body></html>')
  343.             bod = nroot.find('body')
  344.             for child in list(root):
  345.                 child.getparent().remove(child)
  346.                 bod.append(child)
  347.             
  348.             root = nroot
  349.         
  350.         htmls = list(root.xpath('//html'))
  351.         if len(htmls) > 1:
  352.             self.log.warn('Markup contains multiple <html> tags, merging.')
  353.             for h in htmls:
  354.                 p = h.getparent()
  355.                 if hasattr(p, 'remove'):
  356.                     p.remove(h)
  357.                     continue
  358.             
  359.             bodies = root.xpath('//body')
  360.             heads = root.xpath('//head')
  361.             for x in root:
  362.                 root.remove(x)
  363.             
  364.             (head, body) = map(root.makeelement, ('head', 'body'))
  365.             for h in heads:
  366.                 for x in h:
  367.                     h.remove(x)
  368.                     head.append(x)
  369.                 
  370.             
  371.             for b in bodies:
  372.                 for x in b:
  373.                     b.remove(x)
  374.                     body.append(x)
  375.                 
  376.             
  377.             (root.append(head), root.append(body))
  378.         
  379.         for x in root.xpath('//script'):
  380.             x.getparent().remove(x)
  381.         
  382.         head = root.xpath('//head')
  383.         if head:
  384.             head = head[0]
  385.         else:
  386.             head = root.makeelement('head', { })
  387.             root.insert(0, head)
  388.         head.text = '\n\t'
  389.         link = head.makeelement('link', {
  390.             'type': 'text/css',
  391.             'href': 'styles.css',
  392.             'rel': 'stylesheet' })
  393.         head.insert(0, link)
  394.         link.tail = '\n\t'
  395.         title = head.xpath('descendant::title')
  396.         m = head.makeelement('meta', {
  397.             'http-equiv': 'Content-Type',
  398.             'content': 'text/html; charset=utf-8' })
  399.         head.insert(0, m)
  400.         if not title:
  401.             title = head.makeelement('title', { })
  402.             title.text = self.book_header.title
  403.             title.tail = '\n\t'
  404.             head.insert(0, title)
  405.             head.text = '\n\t'
  406.         
  407.         self.upshift_markup(root)
  408.         guides = root.xpath('//guide')
  409.         guide = None if guides else None
  410.         metadata_elems = root.xpath('//metadata')
  411.         if metadata_elems and self.book_header.exth is None:
  412.             self.read_embedded_metadata(root, metadata_elems[0], guide)
  413.         
  414.         for elem in guides + metadata_elems:
  415.             elem.getparent().remove(elem)
  416.         
  417.         fname = self.name.encode('ascii', 'replace')
  418.         fname = re.sub('[\\x08\\x15\\0]+', '', fname)
  419.         htmlfile = os.path.join(output_dir, ascii_filename(fname) + '.html')
  420.         
  421.         try:
  422.             for ref in guide.xpath('descendant::reference'):
  423.                 if ref.attrib.has_key('href'):
  424.                     ref.attrib['href'] = os.path.basename(htmlfile) + ref.attrib['href']
  425.                     continue
  426.         except AttributeError:
  427.             pass
  428.  
  429.         parse_cache[htmlfile] = root
  430.         self.htmlfile = htmlfile
  431.         ncx = cStringIO.StringIO()
  432.         (opf, ncx_manifest_entry) = self.create_opf(htmlfile, guide, root)
  433.         self.created_opf_path = os.path.splitext(htmlfile)[0] + '.opf'
  434.         opf.render(open(self.created_opf_path, 'wb'), ncx, ncx_manifest_entry = ncx_manifest_entry)
  435.         ncx = ncx.getvalue()
  436.         if ncx:
  437.             ncx_path = os.path.join(os.path.dirname(htmlfile), 'toc.ncx')
  438.             open(ncx_path, 'wb').write(ncx)
  439.         
  440.         
  441.         try:
  442.             s = _[1]
  443.             s.write(self.base_css_rules + '\n\n')
  444.             for cls, rule in self.tag_css_rules.items():
  445.                 s.write('.%s { %s }\n\n' % (cls, rule))
  446.         finally:
  447.             pass
  448.  
  449.  
  450.     
  451.     def read_embedded_metadata(self, root, elem, guide):
  452.         raw = '<?xml version="1.0" encoding="utf-8" ?>\n<package>' + html.tostring(elem, encoding = 'utf-8') + '</package>'
  453.         stream = cStringIO.StringIO(raw)
  454.         opf = OPF(stream)
  455.         self.embedded_mi = MetaInformation(opf)
  456.         if guide is not None:
  457.             for ref in guide.xpath('descendant::reference'):
  458.                 if 'cover' in ref.get('type', '').lower():
  459.                     href = ref.get('href', '')
  460.                     if href.startswith('#'):
  461.                         href = href[1:]
  462.                     
  463.                     anchors = root.xpath('//*[@id="%s"]' % href)
  464.                     if anchors:
  465.                         cpos = anchors[0]
  466.                         reached = False
  467.                         for elem in root.iter():
  468.                             if elem is cpos:
  469.                                 reached = True
  470.                             
  471.                             if reached and elem.tag == 'img':
  472.                                 cover = elem.get('src', None)
  473.                                 self.embedded_mi.cover = cover
  474.                                 elem.getparent().remove(elem)
  475.                                 break
  476.                                 continue
  477.                         
  478.                     
  479.                     break
  480.                     continue
  481.             
  482.         
  483.  
  484.     
  485.     def cleanup_html(self):
  486.         self.log.debug('Cleaning up HTML...')
  487.         self.processed_html = re.sub('<div height="0(pt|px|ex|em|%){0,1}"></div>', '', self.processed_html)
  488.         if self.book_header.ancient and '<html' not in self.mobi_html[:300].lower():
  489.             self.processed_html = '<html><p>' + self.processed_html.replace('\n\n', '<p>') + '</html>'
  490.         
  491.         self.processed_html = self.processed_html.replace('\r\n', '\n')
  492.         self.processed_html = self.processed_html.replace('> <', '>\n<')
  493.         self.processed_html = self.processed_html.replace('<mbp: ', '<mbp:')
  494.         self.processed_html = re.sub('<?xml[^>]*>', '', self.processed_html)
  495.  
  496.     
  497.     def remove_random_bytes(self, html):
  498.         return re.sub('\x14|\x15|\x19|\x1c|\x1d|\xef|\x12|\x13|\xec|\x08', '', html)
  499.  
  500.     
  501.     def ensure_unit(self, raw, unit = 'px'):
  502.         if re.search('\\d+$', raw) is not None:
  503.             raw += unit
  504.         
  505.         return raw
  506.  
  507.     
  508.     def upshift_markup(self, root):
  509.         self.log.debug('Converting style information to CSS...')
  510.         size_map = {
  511.             'xx-small': '0.5',
  512.             'x-small': '1',
  513.             'small': '2',
  514.             'medium': '3',
  515.             'large': '4',
  516.             'x-large': '5',
  517.             'xx-large': '6' }
  518.         mobi_version = self.book_header.mobi_version
  519.         for x in root.xpath('//ncx'):
  520.             x.getparent().remove(x)
  521.         
  522.         for i, tag in enumerate(root.iter(etree.Element)):
  523.             tag.attrib.pop('xmlns', '')
  524.             for x in tag.attrib:
  525.                 if ':' in x:
  526.                     del tag.attrib[x]
  527.                     continue
  528.             
  529.             if tag.tag in ('country-region', 'place', 'placetype', 'placename', 'state', 'city', 'street', 'address', 'content', 'form'):
  530.                 tag.tag = None if tag.tag in ('content', 'form') else 'span'
  531.                 for key in tag.attrib.keys():
  532.                     tag.attrib.pop(key)
  533.                 
  534.                 continue
  535.             
  536.             styles = []
  537.             attrib = tag.attrib
  538.             if attrib.has_key('style'):
  539.                 style = attrib.pop('style').strip()
  540.                 if style:
  541.                     styles.append(style)
  542.                 
  543.             
  544.             if attrib.has_key('height'):
  545.                 height = attrib.pop('height').strip()
  546.                 if height and '<' not in height and '>' not in height and re.search('\\d+', height):
  547.                     if tag.tag in ('table', 'td', 'tr'):
  548.                         pass
  549.                     elif tag.tag == 'img':
  550.                         tag.set('height', height)
  551.                     else:
  552.                         styles.append('margin-top: %s' % self.ensure_unit(height))
  553.                 
  554.             
  555.             if attrib.has_key('width'):
  556.                 width = attrib.pop('width').strip()
  557.                 if width and re.search('\\d+', width):
  558.                     if tag.tag in ('table', 'td', 'tr'):
  559.                         pass
  560.                     elif tag.tag == 'img':
  561.                         tag.set('width', width)
  562.                     else:
  563.                         styles.append('text-indent: %s' % self.ensure_unit(width))
  564.                         if width.startswith('-'):
  565.                             styles.append('margin-left: %s' % self.ensure_unit(width[1:]))
  566.                         
  567.                 
  568.             
  569.             if attrib.has_key('align'):
  570.                 align = attrib.pop('align').strip()
  571.                 if align:
  572.                     align = align.lower()
  573.                     if align == 'baseline':
  574.                         styles.append('vertical-align: ' + align)
  575.                     else:
  576.                         styles.append('text-align: %s' % align)
  577.                 
  578.             
  579.             if tag.tag == 'hr':
  580.                 if mobi_version == 1:
  581.                     tag.tag = 'div'
  582.                     styles.append('page-break-before: always')
  583.                     styles.append('display: block')
  584.                     styles.append('margin: 0')
  585.                 
  586.             elif tag.tag == 'i':
  587.                 tag.tag = 'span'
  588.                 tag.attrib['class'] = 'italic'
  589.             elif tag.tag == 'b':
  590.                 tag.tag = 'span'
  591.                 tag.attrib['class'] = 'bold'
  592.             elif tag.tag == 'font':
  593.                 sz = tag.get('size', '').lower()
  594.                 
  595.                 try:
  596.                     float(sz)
  597.                 except ValueError:
  598.                     if sz in size_map.keys():
  599.                         attrib['size'] = size_map[sz]
  600.                     
  601.                 except:
  602.                     sz in size_map.keys()
  603.                 
  604.  
  605.             None<EXCEPTION MATCH>ValueError
  606.             if tag.tag == 'img':
  607.                 recindex = None
  608.                 for attr in self.IMAGE_ATTRS:
  609.                     if not attrib.pop(attr, None):
  610.                         pass
  611.                     recindex = recindex
  612.                 
  613.                 if recindex is not None:
  614.                     attrib['src'] = 'images/%s.jpg' % recindex
  615.                 
  616.                 for attr in ('width', 'height'):
  617.                     if attr in attrib:
  618.                         val = attrib[attr]
  619.                         if val.lower().endswith('em'):
  620.                             
  621.                             try:
  622.                                 nval = float(val[:-2])
  623.                                 nval *= 16 * (168.451 / 72)
  624.                                 attrib[attr] = '%dpx' % int(nval)
  625.                             del attrib[attr]
  626.  
  627.                         elif val.lower().endswith('%'):
  628.                             del attrib[attr]
  629.                         
  630.                     val.lower().endswith('em')
  631.                 
  632.             elif tag.tag == 'pre':
  633.                 if not tag.text:
  634.                     tag.tag = 'div'
  635.                 
  636.             
  637.             if 'filepos-id' in attrib:
  638.                 attrib['id'] = attrib.pop('filepos-id')
  639.                 if 'name' in attrib and attrib['name'] != attrib['id']:
  640.                     attrib['name'] = attrib['id']
  641.                 
  642.             
  643.             if 'filepos' in attrib:
  644.                 filepos = attrib.pop('filepos')
  645.                 
  646.                 try:
  647.                     attrib['href'] = '#filepos%d' % int(filepos)
  648.                 except ValueError:
  649.                     pass
  650.                 except:
  651.                     None<EXCEPTION MATCH>ValueError
  652.                 
  653.  
  654.             None<EXCEPTION MATCH>ValueError
  655.             if styles:
  656.                 ncls = None
  657.                 rule = '; '.join(styles)
  658.                 for sel, srule in self.tag_css_rules.items():
  659.                     if srule == rule:
  660.                         ncls = sel
  661.                         break
  662.                         continue
  663.                 
  664.                 if ncls is None:
  665.                     ncls = 'calibre_%d' % i
  666.                     self.tag_css_rules[ncls] = rule
  667.                 
  668.                 cls = attrib.get('class', '')
  669.                 cls = None + cls if cls else '' + ncls
  670.                 attrib['class'] = cls
  671.                 continue
  672.         
  673.  
  674.     
  675.     def create_opf(self, htmlfile, guide = None, root = None):
  676.         mi = getattr(self.book_header.exth, 'mi', self.embedded_mi)
  677.         if mi is None:
  678.             mi = MetaInformation(self.book_header.title, [
  679.                 _('Unknown')])
  680.         
  681.         opf = OPFCreator(os.path.dirname(htmlfile), mi)
  682.         if hasattr(self.book_header.exth, 'cover_offset'):
  683.             opf.cover = 'images/%05d.jpg' % (self.book_header.exth.cover_offset + 1)
  684.         elif mi.cover is not None:
  685.             opf.cover = mi.cover
  686.         else:
  687.             opf.cover = 'images/00001.jpg'
  688.             if not os.path.exists(os.path.join(os.path.dirname(htmlfile), *opf.cover.split('/'))):
  689.                 opf.cover = None
  690.             
  691.         cover = opf.cover
  692.         cover_copied = None
  693.         if cover is not None:
  694.             cover = cover.replace('/', os.sep)
  695.             if os.path.exists(cover):
  696.                 ncover = 'images' + os.sep + 'calibre_cover.jpg'
  697.                 if os.path.exists(ncover):
  698.                     os.remove(ncover)
  699.                 
  700.                 shutil.copyfile(cover, ncover)
  701.                 cover_copied = os.path.abspath(ncover)
  702.                 opf.cover = ncover.replace(os.sep, '/')
  703.             
  704.         
  705.         manifest = [
  706.             (htmlfile, 'application/xhtml+xml'),
  707.             (os.path.abspath('styles.css'), 'text/css')]
  708.         bp = os.path.dirname(htmlfile)
  709.         added = set([])
  710.         for i in getattr(self, 'image_names', []):
  711.             path = os.path.join(bp, 'images', i)
  712.             added.add(path)
  713.             manifest.append((path, 'image/jpeg'))
  714.         
  715.         if cover_copied is not None:
  716.             manifest.append((cover_copied, 'image/jpeg'))
  717.         
  718.         opf.create_manifest(manifest)
  719.         opf.create_spine([
  720.             os.path.basename(htmlfile)])
  721.         toc = None
  722.         if guide is not None:
  723.             opf.create_guide(guide)
  724.             for ref in opf.guide:
  725.                 if ref.type.lower() == 'toc':
  726.                     toc = ref.href()
  727.                     continue
  728.             
  729.         
  730.         ncx_manifest_entry = None
  731.         if toc:
  732.             ncx_manifest_entry = 'toc.ncx'
  733.             elems = root.xpath('//*[@id="%s"]' % toc.partition('#')[-1])
  734.             tocobj = None
  735.             ent_pat = re.compile('&(\\S+?);')
  736.             if elems:
  737.                 tocobj = TOC()
  738.                 reached = False
  739.                 for x in root.iter():
  740.                     if x == elems[-1]:
  741.                         reached = True
  742.                         continue
  743.                     
  744.                     if reached and x.tag == 'a':
  745.                         href = x.get('href', '')
  746.                         if href and re.match('\\w+://', href) is None:
  747.                             
  748.                             try:
  749.                                 text = []([ t.strip() for t in x.xpath('descendant::text()') ])
  750.                             except:
  751.                                 text = ''
  752.  
  753.                             text = ent_pat.sub(entity_to_unicode, text)
  754.                             tocobj.add_item(toc.partition('#')[0], href[1:], text)
  755.                         
  756.                     
  757.                     if reached and x.get('class', None) == 'mbp_pagebreak':
  758.                         break
  759.                         continue
  760.                 
  761.             
  762.             if tocobj is not None:
  763.                 opf.set_toc(tocobj)
  764.             
  765.         
  766.         return (opf, ncx_manifest_entry)
  767.  
  768.     
  769.     def sizeof_trailing_entries(self, data):
  770.         
  771.         def sizeof_trailing_entry(ptr, psize):
  772.             (bitpos, result) = (0, 0)
  773.             while True:
  774.                 v = ord(ptr[psize - 1])
  775.                 result |= (v & 127) << bitpos
  776.                 bitpos += 7
  777.                 psize -= 1
  778.                 if v & 128 != 0 and bitpos >= 28 or psize == 0:
  779.                     return result
  780.                 continue
  781.                 psize == 0
  782.  
  783.         num = 0
  784.         size = len(data)
  785.         flags = self.book_header.extra_flags >> 1
  786.         while flags:
  787.             if flags & 1:
  788.                 num += sizeof_trailing_entry(data, size - num)
  789.             
  790.             flags >>= 1
  791.         if self.book_header.extra_flags & 1:
  792.             num += (ord(data[size - num - 1]) & 3) + 1
  793.         
  794.         return num
  795.  
  796.     
  797.     def text_section(self, index):
  798.         data = self.sections[index][0]
  799.         trail_size = self.sizeof_trailing_entries(data)
  800.         return data[:len(data) - trail_size]
  801.  
  802.     
  803.     def extract_text(self):
  804.         self.log.debug('Extracting text...')
  805.         text_sections = [ self.text_section(i) for i in range(1, self.book_header.records + 1) ]
  806.         processed_records = list(range(0, self.book_header.records + 1))
  807.         self.mobi_html = ''
  808.         if self.book_header.compression_type == 'DH':
  809.             huffs = [ self.sections[i][0] for i in range(self.book_header.huff_offset, self.book_header.huff_offset + self.book_header.huff_number) ]
  810.             processed_records += list(range(self.book_header.huff_offset, self.book_header.huff_offset + self.book_header.huff_number))
  811.             huff = HuffReader(huffs)
  812.             self.mobi_html = huff.decompress(text_sections)
  813.         elif self.book_header.compression_type == '\x00\x02':
  814.             for section in text_sections:
  815.                 self.mobi_html += decompress_doc(section)
  816.             
  817.         elif self.book_header.compression_type == '\x00\x01':
  818.             self.mobi_html = ''.join(text_sections)
  819.         else:
  820.             raise MobiError('Unknown compression algorithm: %s' % repr(self.book_header.compression_type))
  821.         self.mobi_html = self.mobi_html.replace('\x00', '')
  822.         if self.book_header.codec == 'cp1252':
  823.             self.mobi_html = self.mobi_html.replace('\x1e', '')
  824.             self.mobi_html = self.mobi_html.replace('\x02', '')
  825.         
  826.         return processed_records
  827.  
  828.     
  829.     def replace_page_breaks(self):
  830.         self.processed_html = self.PAGE_BREAK_PAT.sub('<div class="mbp_pagebreak" />', self.processed_html)
  831.  
  832.     
  833.     def add_anchors(self):
  834.         self.log.debug('Adding anchors...')
  835.         positions = set([])
  836.         link_pattern = re.compile('<[^<>]+filepos=[\'"]{0,1}(\\d+)[^<>]*>', re.IGNORECASE)
  837.         for match in link_pattern.finditer(self.mobi_html):
  838.             positions.add(int(match.group(1)))
  839.         
  840.         pos = 0
  841.         self.processed_html = ''
  842.         end_tag_re = re.compile('<\\s*/')
  843.         for end in sorted(positions):
  844.             if end == 0:
  845.                 continue
  846.             
  847.             oend = end
  848.             l = self.mobi_html.find('<', end)
  849.             r = self.mobi_html.find('>', end)
  850.             anchor = '<a id="filepos%d"></a>'
  851.             if r > -1:
  852.                 if r < l and l == end or l == -1:
  853.                     p = self.mobi_html.rfind('<', 0, end + 1)
  854.                     if pos < end and p > -1 and not end_tag_re.match(self.mobi_html[p:r]) and not self.mobi_html[p:r + 1].endswith('/>'):
  855.                         anchor = ' filepos-id="filepos%d"'
  856.                         end = r
  857.                     else:
  858.                         end = r + 1
  859.                 
  860.             self.processed_html += self.mobi_html[pos:end] + anchor % oend
  861.             pos = end
  862.         
  863.         self.processed_html += self.mobi_html[pos:]
  864.         self.processed_html = re.sub('&([^;]*?)(<a id="filepos\\d+"></a>)([^;]*);', '&\\1\\3;\\2', self.processed_html)
  865.  
  866.     
  867.     def extract_images(self, processed_records, output_dir):
  868.         self.log.debug('Extracting images...')
  869.         output_dir = os.path.abspath(os.path.join(output_dir, 'images'))
  870.         if not os.path.exists(output_dir):
  871.             os.makedirs(output_dir)
  872.         
  873.         image_index = 0
  874.         self.image_names = []
  875.         start = getattr(self.book_header, 'first_image_index', -1)
  876.         if start > self.num_sections or start < 0:
  877.             start = 0
  878.         
  879.         for i in range(start, self.num_sections):
  880.             if i in processed_records:
  881.                 continue
  882.             
  883.             processed_records.append(i)
  884.             data = self.sections[i][0]
  885.             buf = cStringIO.StringIO(data)
  886.             image_index += 1
  887.             
  888.             try:
  889.                 im = PILImage.open(buf)
  890.                 im = im.convert('RGB')
  891.             except IOError:
  892.                 continue
  893.  
  894.             path = os.path.join(output_dir, '%05d.jpg' % image_index)
  895.             self.image_names.append(os.path.basename(path))
  896.             im.save(open(path, 'wb'), format = 'JPEG')
  897.         
  898.  
  899.  
  900.  
  901. def get_metadata(stream):
  902.     Log = Log
  903.     import calibre.utils.logging
  904.     log = Log()
  905.     mi = MetaInformation(os.path.basename(stream.name), [
  906.         _('Unknown')])
  907.     mh = MetadataHeader(stream, log)
  908.     if mh.title and mh.title != _('Unknown'):
  909.         mi.title = mh.title
  910.     
  911.     if hasattr(mh.exth, 'cover_offset'):
  912.         cover_index = mh.first_image_index + mh.exth.cover_offset
  913.         data = mh.section_data(int(cover_index))
  914.     else:
  915.         data = mh.section_data(mh.first_image_index)
  916.     buf = cStringIO.StringIO(data)
  917.     
  918.     try:
  919.         im = PILImage.open(buf)
  920.     except:
  921.         log.exception('Failed to read MOBI cover')
  922.  
  923.     obuf = cStringIO.StringIO()
  924.     im.convert('RGB').save(obuf, format = 'JPEG')
  925.     mi.cover_data = ('jpg', obuf.getvalue())
  926.     return mi
  927.  
  928.