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.

__init__.py 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. from oscar.apps.dynamic_images.cache import DiskCache
  2. from oscar.apps.dynamic_images.exceptions import ResizerConfigurationError, \
  3. ResizerSyntaxError, ResizerFormatError
  4. from oscar.apps.dynamic_images.mods import AutotrimMod, CropMod, ResizeMod
  5. from oscar.apps.dynamic_images.response_backends import DirectResponse
  6. from wsgiref.util import request_uri, application_uri
  7. import Image
  8. import cStringIO
  9. import datetime
  10. import math
  11. import os
  12. import sys
  13. import re
  14. try:
  15. import cStringIO as StringIO
  16. except:
  17. import StringIO
  18. def get_class(kls):
  19. try:
  20. parts = kls.split('.')
  21. module = ".".join(parts[:-1])
  22. m = __import__(module)
  23. for comp in parts[1:]:
  24. m = getattr(m, comp)
  25. return m
  26. except (ImportError, AttributeError), e:
  27. raise ResizerConfigurationError('Error importing class "%s"' % kls)
  28. def error404(path, start_response):
  29. """ Returns an error 404 with text giving the requested URL. """
  30. status = '404 NOT FOUND'
  31. output = '404: File Not Found: ' + path + '\n'
  32. response_headers = [('Content-type', 'text/plain')]
  33. start_response(status, response_headers)
  34. return [output]
  35. def error500(path, e, start_response):
  36. """ Returns an error 500 with text giving the requested URL. """
  37. status = '500 Exception'
  38. output = '500: ' + str(e) + '\n'
  39. response_headers = [('Content-type', 'text/plain')]
  40. start_response(status, response_headers)
  41. return [output]
  42. class ImageModifier(object):
  43. """
  44. Modifies an image and saves to a cache location
  45. Output Formats:
  46. extension => ('format', 'mime-type')
  47. the key is the extension appended to the URL,
  48. the value tuple holds PIL's name for the format and the mime-type to serve
  49. with
  50. """
  51. output_formats = {
  52. 'jpeg': ('JPEG', 'image/jpeg'),
  53. 'jpg': ('JPEG', 'image/jpeg'),
  54. 'gif': ('GIF', 'image/gif'),
  55. 'png': ('PNG', 'image/png'),
  56. }
  57. # When we process an image, these modifications are applied in order
  58. installed_modifications = (
  59. AutotrimMod,
  60. CropMod,
  61. ResizeMod,
  62. )
  63. quality = 80
  64. process_check = re.compile(r'^(?P<filename>.+?)\.(?P<params>[a-z0-9]+-[a-z0-9]+(.+?)*)\.(?P<type>[a-z0-9]+)$').search
  65. convert_check = re.compile(r'^(?P<filename>.+?)\.to\.(?P<type>[a-z0-9]+)$').search
  66. def __init__(self, url, config):
  67. if config.get('installed_mods'):
  68. self.installed_modifications = config['installed_mods']
  69. self._url = url
  70. self._image_root = config['asset_root']
  71. self._process_path()
  72. def _process_path(self):
  73. """
  74. Extracts parameters from the image path
  75. Valid syntax:
  76. - /path/to/image.ext (serve image unchanged)
  77. - /path/to/image.ext.to.newext (change format of image)
  78. - /path/to/image.ext.options-string.newext (change format and modify
  79. image)
  80. Format of options string is:
  81. key1-value1_key2-value2_key3-value3
  82. """
  83. path, name = os.path.split(self._url)
  84. p_result = self.process_check(name)
  85. c_result = self.convert_check(name)
  86. if p_result:
  87. filename = p_result.group('filename')
  88. type = p_result.group('type')
  89. params = p_result.group('params')
  90. param_parts = params.split('_')
  91. try:
  92. params = dict(
  93. [(x.split("-")[0], x.split("-")[1]) for x in param_parts])
  94. except IndexError:
  95. raise ResizerSyntaxError("Invalid filename syntax")
  96. elif c_result:
  97. filename = c_result.group('filename')
  98. type = c_result.group('type')
  99. params = {}
  100. else:
  101. filename = self._url
  102. type = os.path.splitext(name)[1][1:]
  103. params = {}
  104. params['type'] = type
  105. self._params = params
  106. self.source_filename = os.path.join(path,filename)
  107. if self._params['type'] not in self.output_formats:
  108. raise ResizerFormatError("Invalid output format")
  109. def source_path(self):
  110. return os.path.join(self._image_root, self.source_filename)
  111. def generate_image(self):
  112. source = Image.open(self.source_path())
  113. if (self._params['type'] == 'png'):
  114. source = source.convert("RGBA")
  115. else:
  116. source = source.convert("RGB")
  117. # Iterate over the installed modifications and apply them to the image
  118. for mod in self.installed_modifications:
  119. source = mod(source, self._params).apply()
  120. output = StringIO.StringIO()
  121. source.save(output, self.get_type()[0], quality=self.quality)
  122. output.seek(0)
  123. data = output.read()
  124. return data
  125. def get_type(self):
  126. return self.output_formats[self._params['type']]
  127. def get_mime_type(self):
  128. return self.get_type()[1]
  129. class BaseImageHandler(object):
  130. """
  131. This can be called by a WSGI script, or via DjangoImageHandler.
  132. Django version is handy for local development, but adds unnecessary
  133. overhead to production
  134. """
  135. modifier = ImageModifier
  136. cache = DiskCache
  137. response_backend = DirectResponse
  138. def build_sendfile_response(self, metadata, modifier, start_response):
  139. pass
  140. def __call__(self, environ, start_response):
  141. path = environ.get('PATH_INFO')
  142. config = environ.get('MEDIA_CONFIG')
  143. # Don't bother processing stuff we know is invalid
  144. if path == '/' or path == '/favicon.ico':
  145. return error404(path, start_response)
  146. path = path[1:]
  147. if config.get('cache_backend'):
  148. self.cache = get_class(config['cache_backend'])
  149. if config.get('response_backend'):
  150. self.response_backend = get_class(config['response_backend'])
  151. if config.get('installed_mods'):
  152. mod_overrides = []
  153. for v in config['installed_mods']:
  154. mod_overrides.append(get_class(v))
  155. config.installed_mods = tuple(mod_overrides)
  156. try:
  157. c = self.cache(path, config)
  158. m = self.modifier(path, config)
  159. except (ResizerSyntaxError, ResizerFormatError), e:
  160. return error500(path, e, start_response)
  161. try:
  162. if not c.check(m.source_path()):
  163. data = m.generate_image()
  164. c.write(data)
  165. return self.response_backend(config, m.get_mime_type(),c,start_response).build_response()
  166. except Exception, e:
  167. return error404(path, start_response)
  168. class DjangoImageHandler(BaseImageHandler):
  169. def __call__(self, request):
  170. from django.http import HttpResponse
  171. from django.conf import settings
  172. env = request.META.copy()
  173. config = settings.DYNAMIC_MEDIA_CONFIG
  174. prefix = settings.DYNAMIC_URL_PREFIX
  175. env['MEDIA_CONFIG'] = config
  176. env['PATH_INFO'] = env['PATH_INFO'][len(prefix) + 1:]
  177. django_response = HttpResponse()
  178. def start_response(status, headers):
  179. status = status.split(' ', 1)[0]
  180. django_response.status_code = int(status)
  181. for header, value in headers:
  182. django_response[header] = value
  183. response = super(DjangoImageHandler, self).__call__(env, start_response)
  184. django_response.content = "\n".join(response)
  185. return django_response