Django Library PyGal
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

image.py 9.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. from django.conf import settings
  2. import fstools
  3. import io
  4. import logging
  5. import mimetypes
  6. from ..models import get_item_type, TYPE_IMAGE, TYPE_VIDEO
  7. import os
  8. from PIL import Image, ImageEnhance, ExifTags
  9. import platform
  10. import pygal
  11. import subprocess
  12. # Get a logger instance
  13. logger = logging.getLogger("CACHING")
  14. def get_image_class(full_path):
  15. return {
  16. TYPE_IMAGE: image,
  17. TYPE_VIDEO: video,
  18. }.get(get_item_type(full_path), other)
  19. def get_image_instance(full_path, request):
  20. return get_image_class(full_path)(full_path, request)
  21. class mm_image(object):
  22. JOIN_TOP_LEFT = 1
  23. JOIN_TOP_RIGHT = 2
  24. JOIN_BOT_LEFT = 3
  25. JOIN_BOT_RIGHT = 4
  26. JOIN_CENTER = 5
  27. def __init__(self, imagepath_handle_image):
  28. self.imagepath_handle_image = imagepath_handle_image
  29. self.__image__ = None
  30. #
  31. def __init_image__(self):
  32. if self.__image__ is None:
  33. if type(self.imagepath_handle_image) is mm_image:
  34. self.__image__ = self.imagepath_handle_image.__image__
  35. elif type(self.imagepath_handle_image) is Image.Image:
  36. self.__image__ = self.imagepath_handle_image
  37. else:
  38. self.__image__ = Image.open(self.imagepath_handle_image)
  39. def orientation(self):
  40. self.__init_image__()
  41. exif_tags = dict((v, k) for k, v in ExifTags.TAGS.items())
  42. try:
  43. return dict(self.__image__._getexif().items())[exif_tags['Orientation']]
  44. except AttributeError:
  45. return 1
  46. except KeyError:
  47. return 1
  48. def copy(self):
  49. self.__init_image__()
  50. return mm_image(self.__image__.copy())
  51. def save(self, *args, **kwargs):
  52. self.__init_image__()
  53. im = self.__image__.copy().convert('RGB')
  54. im.save(*args, **kwargs)
  55. def resize(self, max_size):
  56. self.__init_image__()
  57. #
  58. # resize
  59. #
  60. x, y = self.__image__.size
  61. xy_max = max(x, y)
  62. self.__image__ = self.__image__.resize((int(x * float(max_size) / xy_max), int(y * float(max_size) / xy_max)), Image.NEAREST).rotate(0)
  63. def rotate_by_orientation(self, orientation):
  64. self.__init_image__()
  65. #
  66. # rotate
  67. #
  68. angle = {3: 180, 6: 270, 8: 90}.get(orientation)
  69. if angle is not None:
  70. self.__image__ = self.__image__.rotate(angle, expand=True)
  71. def __rgba_copy__(self):
  72. self.__init_image__()
  73. if self.__image__.mode != 'RGBA':
  74. return self.__image__.convert('RGBA')
  75. else:
  76. return self.__image__.copy()
  77. def join(self, image, joint_pos=JOIN_TOP_RIGHT, opacity=0.7):
  78. """
  79. This joins another picture to this one.
  80. :param picture_edit picture: The picture to be joint.
  81. :param joint_pos: The position of picture in this picture. See also self.JOIN_*
  82. :param float opacity: The opacity of picture when joint (value between 0 and 1).
  83. .. note::
  84. joint_pos makes only sense if picture is smaller than this picture.
  85. """
  86. self.__init_image__()
  87. #
  88. im2 = image.__rgba_copy__()
  89. # change opacity of im2
  90. alpha = im2.split()[3]
  91. alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
  92. im2.putalpha(alpha)
  93. self.__image__ = self.__rgba_copy__()
  94. # create a transparent layer
  95. layer = Image.new('RGBA', self.__image__.size, (0, 0, 0, 0))
  96. # draw im2 in layer
  97. if joint_pos == self.JOIN_TOP_LEFT:
  98. layer.paste(im2, (0, 0))
  99. elif joint_pos == self.JOIN_TOP_RIGHT:
  100. layer.paste(im2, ((self.__image__.size[0] - im2.size[0]), 0))
  101. elif joint_pos == self.JOIN_BOT_LEFT:
  102. layer.paste(im2, (0, (self.__image__.size[1] - im2.size[1])))
  103. elif joint_pos == self.JOIN_BOT_RIGHT:
  104. layer.paste(im2, ((self.__image__.size[0] - im2.size[0]), (self.__image__.size[1] - im2.size[1])))
  105. elif joint_pos == self.JOIN_CENTER:
  106. layer.paste(im2, (int((self.__image__.size[0] - im2.size[0]) / 2), int((self.__image__.size[1] - im2.size[1]) / 2)))
  107. self.__image__ = Image.composite(layer, self.__image__, layer)
  108. def image_data(self):
  109. self.__init_image__()
  110. #
  111. # create return value
  112. #
  113. im = self.__image__.copy().convert('RGB')
  114. output = io.BytesIO()
  115. im.save(output, format='JPEG')
  116. return output.getvalue()
  117. class mm_video(object):
  118. def __init__(self, full_path):
  119. self.full_path = full_path
  120. def image(self):
  121. if platform.system() == 'Linux':
  122. cmd = 'ffmpeg -ss 0.5 -i "' + self.full_path + '" -vframes 1 -f image2pipe pipe:1 2> /dev/null'
  123. else:
  124. cmd = 'ffmpeg -ss 0.5 -i "' + self.full_path + '" -vframes 1 -f image2pipe pipe:1 2> NULL'
  125. data = subprocess.check_output(cmd, shell=True)
  126. ffmpeg_handle = io.BytesIO(data)
  127. im = Image.open(ffmpeg_handle)
  128. return mm_image(im.copy())
  129. class base_item(object):
  130. MIME_TYPES = {
  131. '.ada': 'text/x/adasrc',
  132. '.hex': 'text/x/hex',
  133. '.jpg': 'image/x/generic',
  134. '.jpeg': 'image/x/generic',
  135. '.jpe': 'image/x/generic',
  136. '.png': 'image/x/generic',
  137. '.tif': 'image/x/generic',
  138. '.tiff': 'image/x/generic',
  139. '.gif': 'image/x/generic',
  140. '.avi': 'video/x/generic',
  141. '.mpg': 'video/x/generic',
  142. '.mpeg': 'video/x/generic',
  143. '.mpe': 'video/x/generic',
  144. '.mov': 'video/x/generic',
  145. '.qt': 'video/x/generic',
  146. '.mp4': 'video/x/generic',
  147. '.webm': 'video/x/generic',
  148. '.ogv': 'video/x/generic',
  149. '.flv': 'video/x/generic',
  150. '.3gp': 'video/x/generic',
  151. }
  152. def __init__(self, full_path, request):
  153. self.full_path = full_path
  154. self.request = request
  155. self.rel_path = pygal.get_rel_path(full_path)
  156. #
  157. ext = os.path.splitext(self.full_path)[1].lower()
  158. self.mime_type = self.MIME_TYPES.get(ext, mimetypes.types_map.get(ext, 'unknown'))
  159. def __cache_image_folder__(self, rel_path):
  160. return os.path.join(settings.XNAIL_ROOT, rel_path.replace('_', '__').replace('/', '_'))
  161. def __cache_image_name__(self, max_size):
  162. filename = '%04d_%02x_%s.jpg' % (max_size, self.XNAIL_VERSION_NUMBER, fstools.uid(self.full_path, None))
  163. return os.path.join(self.__cache_image_folder__(self.rel_path), filename)
  164. def __delete_cache_image__(self, max_size):
  165. for fn in fstools.filelist(self.__cache_image_folder__(self.rel_path), '%04d*' % max_size):
  166. try:
  167. os.remove(fn)
  168. except OSError:
  169. pass # possibly file is already removed by another process
  170. class image(base_item, mm_image):
  171. XNAIL_VERSION_NUMBER = 1
  172. def __init__(self, *args, **kwargs):
  173. base_item.__init__(self, *args, **kwargs)
  174. mm_image.__init__(self, self.full_path)
  175. self.mime_type_xnails = mimetypes.types_map.get('.jpg', 'unknown')
  176. def get_resized_image_data(self, max_size):
  177. cache_filename = self.__cache_image_name__(max_size)
  178. if not os.path.exists(cache_filename):
  179. logger.info('Creating xnail-%d for %s', max_size, self.rel_path)
  180. self.__delete_cache_image__(max_size)
  181. im = self.copy()
  182. im.resize(max_size)
  183. im.rotate_by_orientation(self.orientation())
  184. #
  185. # create cache file
  186. #
  187. fstools.mkdir(os.path.dirname(cache_filename))
  188. with open(cache_filename, 'wb') as fh:
  189. im.save(fh, format='JPEG')
  190. #
  191. return im.image_data()
  192. else:
  193. return open(cache_filename, 'rb').read()
  194. def thumbnail_picture(self):
  195. return self.get_resized_image_data(pygal.get_thumbnail_max_size(self.request))
  196. def webnail_picture(self):
  197. return self.get_resized_image_data(pygal.get_webnail_size(self.request))
  198. class video(base_item, mm_video):
  199. XNAIL_VERSION_NUMBER = 1
  200. def __init__(self, *args, **kwargs):
  201. base_item.__init__(self, *args, **kwargs)
  202. mm_video.__init__(self, self.full_path)
  203. self.mime_type_xnails = mimetypes.types_map.get('.jpg', 'unknown')
  204. def get_resized_image_data(self, max_size):
  205. cache_filename = self.__cache_image_name__(max_size)
  206. if not os.path.exists(cache_filename):
  207. logger.info('Creating xnail-%d for %s', max_size, self.rel_path)
  208. self.__delete_cache_image__(max_size)
  209. im = self.image()
  210. im.resize(max_size)
  211. overlay = mm_image(os.path.join(os.path.dirname(__file__), 'video.png'))
  212. im.join(overlay)
  213. #
  214. # create cache file
  215. #
  216. fstools.mkdir(os.path.dirname(cache_filename))
  217. with open(cache_filename, 'wb') as fh:
  218. im.save(fh, format='JPEG')
  219. #
  220. return im.image_data()
  221. else:
  222. return open(cache_filename, 'rb').read()
  223. def thumbnail_picture(self):
  224. return image.thumbnail_picture(self)
  225. def webnail_picture(self):
  226. return image.webnail_picture(self)
  227. class other(base_item):
  228. def __init__(self, *args, **kwargs):
  229. base_item.__init__(self, *args, **kwargs)
  230. self.mime_type_xnails = mimetypes.types_map.get('.png', 'unknown')
  231. def thumbnail_picture(self):
  232. fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mimetype_icons', '%s.png' % (self.mime_type).replace('/', '-'))
  233. if not os.path.exists(fn):
  234. fn = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'mimetype_icons', 'unknown.png')
  235. return open(fn, 'rb').read()
  236. def webnail_picture(self):
  237. return self.thumbnail_picture()