render.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. #!/usr/bin/env python2
  2. # -*- coding: UTF-8 -*-
  3. # File: render.py
  4. # Date: Sun Feb 01 17:46:57 2015 +0800
  5. # Author: Yuxin Wu <[email protected]>
  6. import os
  7. import base64
  8. import glob
  9. import logging
  10. from pyquery import PyQuery
  11. logger = logging.getLogger(__name__)
  12. LIB_PATH = os.path.dirname(os.path.abspath(__file__))
  13. STATIC_PATH = os.path.join(LIB_PATH, 'static')
  14. HTML_FILE = os.path.join(STATIC_PATH, 'TP_INDEX.html')
  15. TIME_HTML_FILE = os.path.join(STATIC_PATH, 'TP_TIME.html')
  16. FRIEND_AVATAR_CSS_FILE = os.path.join(STATIC_PATH, 'avatar.css.tpl')
  17. try:
  18. from csscompressor import compress as css_compress
  19. except ImportError:
  20. css_compress = lambda x: x
  21. from .msg import *
  22. from .utils import ensure_unicode, ProgressReporter, pmap, timing
  23. from .smiley import SmileyProvider
  24. from .msgslice import MessageSlicerByTime, MessageSlicerBySize
  25. TEMPLATES_FILES = {TYPE_MSG: "TP_MSG",
  26. TYPE_IMG: "TP_IMG",
  27. TYPE_SPEAK: "TP_SPEAK",
  28. TYPE_EMOJI: "TP_EMOJI",
  29. TYPE_CUSTOM_EMOJI: "TP_IMG",
  30. TYPE_LINK: "TP_MSG"}
  31. TEMPLATES = {k: ensure_unicode(open(os.path.join(STATIC_PATH, '{}.html'.format(v))).read())
  32. for k, v in TEMPLATES_FILES.iteritems()}
  33. class HTMLRender(object):
  34. def __init__(self, parser, res=None):
  35. self.html = ensure_unicode(open(HTML_FILE).read())
  36. self.time_html = open(TIME_HTML_FILE).read()
  37. self.parser = parser
  38. self.res = res
  39. if self.res is None:
  40. logger.warn("Resource Directory not given. Images / Voice Message won't be displayed.")
  41. self.smiley = SmileyProvider()
  42. css_files = glob.glob(os.path.join(LIB_PATH, 'static/*.css'))
  43. self.css_string = [] # css to add
  44. for css in css_files:
  45. logger.info("Loading {}".format(os.path.basename(css)))
  46. css = ensure_unicode((open(css).read()))
  47. self.css_string.append(css)
  48. js_files = glob.glob(os.path.join(LIB_PATH, 'static/*.js'))
  49. # to load jquery before other js
  50. js_files = sorted(js_files, key=lambda f: 'jquery' in f, reverse=True)
  51. self.js_string = []
  52. for js in js_files:
  53. logger.info("Loading {}".format(os.path.basename(js)))
  54. js = ensure_unicode(open(js).read())
  55. self.js_string.append(js)
  56. @property
  57. def all_css(self):
  58. # call after processing all messages,
  59. # because smiley css need to be included only when necessary
  60. def process(css):
  61. css = css_compress(css)
  62. return u'<style type="text/css">{}</style>'.format(css)
  63. if hasattr(self, 'final_css'):
  64. return self.final_css + process(self.smiley.gen_used_smiley_css())
  65. self.final_css = u"\n".join(map(process, self.css_string))
  66. return self.final_css + process(self.smiley.gen_used_smiley_css())
  67. @property
  68. def all_js(self):
  69. if hasattr(self, 'final_js'):
  70. return self.final_js
  71. def process(js):
  72. # TODO: add js compress
  73. return u'<script type="text/javascript">{}</script>'.format(js)
  74. self.final_js = u"\n".join(map(process, self.js_string))
  75. return self.final_js
  76. #@timing(total=True)
  77. def render_msg(self, msg):
  78. """ render a message, return the html block"""
  79. # TODO for chatroom, add nickname on avatar
  80. sender = u'you ' + msg.get_msg_talker_id() if not msg.isSend else 'me'
  81. format_dict = {'sender_label': sender,
  82. 'time': msg.createTime }
  83. def fallback():
  84. template = TEMPLATES[TYPE_MSG]
  85. content = msg.msg_str()
  86. format_dict['content'] = self.smiley.replace_smileycode(content)
  87. return template.format(**format_dict)
  88. template = TEMPLATES.get(msg.type)
  89. if msg.type == TYPE_SPEAK:
  90. audio_str, duration = self.res.get_voice_mp3(msg.imgPath)
  91. format_dict['voice_duration'] = duration
  92. format_dict['voice_str'] = audio_str
  93. return template.format(**format_dict)
  94. elif msg.type == TYPE_IMG:
  95. # imgPath was original THUMBNAIL_DIRPATH://th_xxxxxxxxx
  96. imgpath = msg.imgPath.split('_')[-1]
  97. if not imgpath:
  98. logger.warn('No imgpath in an image message. Perhaps a bug in wechat.')
  99. return fallback()
  100. bigimgpath = self.parser.imginfo.get(msg.msgSvrId)
  101. fnames = [k for k in [imgpath, bigimgpath] if k]
  102. bigimg, smallimg = self.res.get_img(fnames)
  103. if not smallimg:
  104. logger.warn("No image thumbnail found for {}".format(imgpath))
  105. return fallback()
  106. img = bigimg if bigimg else smallimg
  107. # TODO do not show fancybox when no bigimg found
  108. format_dict['img'] = (img, 'jpeg')
  109. return template.format(**format_dict)
  110. elif msg.type == TYPE_EMOJI:
  111. md5 = msg.imgPath
  112. if md5 in self.parser.internal_emojis:
  113. emoji_img, format = self.res.get_internal_emoji(self.parser.internal_emojis[md5])
  114. else:
  115. if md5 in self.parser.emojis:
  116. group, _ = self.parser.emojis[md5]
  117. else:
  118. group = None
  119. emoji_img, format = self.res.get_emoji(md5, group)
  120. format_dict['emoji_format'] = format
  121. format_dict['emoji_img'] = emoji_img
  122. return template.format(**format_dict)
  123. elif msg.type == TYPE_CUSTOM_EMOJI:
  124. pq = PyQuery(msg.content)
  125. md5 = pq('emoticonmd5').text()
  126. format_dict['img'] = self.res.get_emoji(md5, None)
  127. return template.format(**format_dict)
  128. elif msg.type == TYPE_LINK:
  129. content = msg.msg_str()
  130. # TODO show a short link with long href, if link too long
  131. if content.startswith(u'URL:'):
  132. url = content[4:]
  133. content = u'URL:<a target="_blank" href="{0}">{0}</a>'.format(url)
  134. format_dict['content'] = content
  135. return template.format(**format_dict)
  136. elif msg.type == TYPE_WX_VIDEO:
  137. # TODO: fetch video from resource
  138. return fallback()
  139. return fallback()
  140. def _render_partial_msgs(self, msgs):
  141. """ return single html"""
  142. self.smiley.used_smiley_id.clear()
  143. slicer = MessageSlicerByTime()
  144. slices = slicer.slice(msgs)
  145. blocks = []
  146. for idx, slice in enumerate(slices):
  147. nowtime = slice[0].createTime
  148. if idx == 0 or \
  149. slices[idx - 1][0].createTime.date() != nowtime.date():
  150. timestr = nowtime.strftime("%m/%d %H:%M:%S")
  151. else:
  152. timestr = nowtime.strftime("%H:%M:%S")
  153. blocks.append(self.time_html.format(time=timestr))
  154. blocks.extend([self.render_msg(m) for m in slice])
  155. self.prgs.trigger(len(slice))
  156. # string operation is extremely slow
  157. return self.html.format(extra_css=self.all_css,
  158. extra_js=self.all_js,
  159. talker=msgs[0].talker_name,
  160. messages=u''.join(blocks)
  161. )
  162. def prepare_avatar_css(self, talkers):
  163. avatar_tpl= ensure_unicode(open(FRIEND_AVATAR_CSS_FILE).read())
  164. my_avatar = self.res.get_avatar(self.parser.username)
  165. css = avatar_tpl.format(name='me', avatar=my_avatar)
  166. for talker in talkers:
  167. avatar = self.res.get_avatar(talker)
  168. css += avatar_tpl.format(name=talker, avatar=avatar)
  169. self.css_string.append(css)
  170. def render_msgs(self, msgs):
  171. """ render msgs of one friend, return a list of html"""
  172. talker_id = msgs[0].talker
  173. if msgs[0].is_chatroom():
  174. talkers = set()
  175. for msg in msgs:
  176. talkers.add(msg.get_msg_talker_id())
  177. else:
  178. talkers = set([talker_id])
  179. self.prepare_avatar_css(talkers)
  180. voice_paths = [msg.imgPath for msg in msgs if msg.type == TYPE_SPEAK]
  181. self.res.cache_voice_mp3(voice_paths)
  182. logger.info(u"Rendering {} messages of {}({})".format(
  183. len(msgs), self.parser.contacts[talker_id], talker_id))
  184. self.prgs = ProgressReporter("Render", total=len(msgs))
  185. slice_by_size = MessageSlicerBySize().slice(msgs)
  186. ret = [self._render_partial_msgs(s) for s in slice_by_size]
  187. self.prgs.finish()
  188. return ret
  189. if __name__ == '__main__':
  190. r = HTMLRender()
  191. with open('/tmp/a.html', 'w') as f:
  192. print >> f, r.html.format(style=r.css, talker='talker',
  193. messages='haha')