render.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/env python2
  2. # -*- coding: UTF-8 -*-
  3. # File: render.py
  4. # Date: Wed Mar 25 22:24:53 2015 +0800
  5. # Author: Yuxin Wu <ppwwyyxxc@gmail.com>
  6. import os
  7. import base64
  8. import glob
  9. from pyquery import PyQuery
  10. import logging
  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. img = self.res.get_img(fnames)
  103. if not img:
  104. logger.warn("No image thumbnail found for {}".format(imgpath))
  105. return fallback()
  106. # TODO do not show fancybox when no bigimg found
  107. format_dict['img'] = (img, 'jpeg')
  108. return template.format(**format_dict)
  109. elif msg.type == TYPE_EMOJI:
  110. md5 = msg.imgPath
  111. if md5 in self.parser.internal_emojis:
  112. emoji_img, format = self.res.get_internal_emoji(self.parser.internal_emojis[md5])
  113. else:
  114. if md5 in self.parser.emojis:
  115. group, _ = self.parser.emojis[md5]
  116. else:
  117. group = None
  118. emoji_img, format = self.res.get_emoji(md5, group)
  119. format_dict['emoji_format'] = format
  120. format_dict['emoji_img'] = emoji_img
  121. return template.format(**format_dict)
  122. elif msg.type == TYPE_CUSTOM_EMOJI:
  123. pq = PyQuery(msg.content)
  124. md5 = pq('emoticonmd5').text()
  125. format_dict['img'] = self.res.get_emoji(md5, None)
  126. return template.format(**format_dict)
  127. elif msg.type == TYPE_LINK:
  128. content = msg.msg_str()
  129. # TODO show a short link with long href, if link too long
  130. if content.startswith(u'URL:'):
  131. url = content[4:]
  132. content = u'URL:<a target="_blank" href="{0}">{0}</a>'.format(url)
  133. format_dict['content'] = content
  134. return template.format(**format_dict)
  135. elif msg.type == TYPE_WX_VIDEO:
  136. # TODO: fetch video from resource
  137. return fallback()
  138. return fallback()
  139. def _render_partial_msgs(self, msgs):
  140. """ return single html"""
  141. self.smiley.used_smiley_id.clear()
  142. slicer = MessageSlicerByTime()
  143. slices = slicer.slice(msgs)
  144. blocks = []
  145. for idx, slice in enumerate(slices):
  146. nowtime = slice[0].createTime
  147. if idx == 0 or \
  148. slices[idx - 1][0].createTime.date() != nowtime.date():
  149. timestr = nowtime.strftime("%m/%d %H:%M:%S")
  150. else:
  151. timestr = nowtime.strftime("%H:%M:%S")
  152. blocks.append(self.time_html.format(time=timestr))
  153. blocks.extend([self.render_msg(m) for m in slice])
  154. self.prgs.trigger(len(slice))
  155. # string operation is extremely slow
  156. return self.html.format(extra_css=self.all_css,
  157. extra_js=self.all_js,
  158. talker=msgs[0].talker_name,
  159. messages=u''.join(blocks)
  160. )
  161. def prepare_avatar_css(self, talkers):
  162. avatar_tpl= ensure_unicode(open(FRIEND_AVATAR_CSS_FILE).read())
  163. my_avatar = self.res.get_avatar(self.parser.username)
  164. css = avatar_tpl.format(name='me', avatar=my_avatar)
  165. for talker in talkers:
  166. avatar = self.res.get_avatar(talker)
  167. css += avatar_tpl.format(name=talker, avatar=avatar)
  168. self.css_string.append(css)
  169. def render_msgs(self, msgs):
  170. """ render msgs of one friend, return a list of html"""
  171. talker_id = msgs[0].talker
  172. if msgs[0].is_chatroom():
  173. talkers = set()
  174. for msg in msgs:
  175. talkers.add(msg.get_msg_talker_id())
  176. else:
  177. talkers = set([talker_id])
  178. self.prepare_avatar_css(talkers)
  179. voice_paths = [msg.imgPath for msg in msgs if msg.type == TYPE_SPEAK]
  180. self.res.cache_voice_mp3(voice_paths)
  181. logger.info(u"Rendering {} messages of {}({})".format(
  182. len(msgs), self.parser.contacts[talker_id], talker_id))
  183. self.prgs = ProgressReporter("Render", total=len(msgs))
  184. slice_by_size = MessageSlicerBySize().slice(msgs)
  185. ret = [self._render_partial_msgs(s) for s in slice_by_size]
  186. self.prgs.finish()
  187. return ret
  188. if __name__ == '__main__':
  189. r = HTMLRender()
  190. with open('/tmp/a.html', 'w') as f:
  191. print >> f, r.html.format(style=r.css, talker='talker',
  192. messages='haha')