uploaded.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. from path import Path
  2. from PIL import Image
  3. from django.core.exceptions import ValidationError
  4. from django.utils.translation import ugettext as _
  5. from misago.conf import settings
  6. from . import store
  7. ALLOWED_EXTENSIONS = ('.gif', '.png', '.jpg', '.jpeg')
  8. ALLOWED_MIME_TYPES = ('image/gif', 'image/jpeg', 'image/png', 'image/mpo')
  9. def validate_file_size(uploaded_file):
  10. upload_limit = settings.avatar_upload_limit * 1024
  11. if uploaded_file.size > upload_limit:
  12. raise ValidationError(_("Uploaded file is too big."))
  13. def validate_extension(uploaded_file):
  14. lowercased_name = uploaded_file.name.lower()
  15. for extension in ALLOWED_EXTENSIONS:
  16. if lowercased_name.endswith(extension):
  17. return True
  18. else:
  19. raise ValidationError(_("Uploaded file type is not allowed."))
  20. def validate_mime(uploaded_file):
  21. if uploaded_file.content_type not in ALLOWED_MIME_TYPES:
  22. raise ValidationError(_("Uploaded file type is not allowed."))
  23. def validate_dimensions(uploaded_file):
  24. image = Image.open(uploaded_file)
  25. min_size = max(settings.MISAGO_AVATARS_SIZES)
  26. if min(image.size) < min_size:
  27. message = _("Uploaded image should be at least %(size)s pixels tall and wide.")
  28. raise ValidationError(message % {'size': min_size})
  29. if image.size[0] * image.size[1] > 2000 * 3000:
  30. message = _("Uploaded image is too big.")
  31. raise ValidationError(message)
  32. image_ratio = float(min(image.size)) / float(max(image.size))
  33. if image_ratio < 0.25:
  34. message = _("Uploaded image ratio cannot be greater than 16:9.")
  35. raise ValidationError(message)
  36. return image
  37. def validate_uploaded_file(uploaded_file):
  38. try:
  39. validate_file_size(uploaded_file)
  40. validate_extension(uploaded_file)
  41. validate_mime(uploaded_file)
  42. return validate_dimensions(uploaded_file)
  43. except ValidationError as e:
  44. try:
  45. temporary_file_path = Path(uploaded_file.temporary_file_path())
  46. if temporary_file_path.exists():
  47. temporary_file_path.remove()
  48. except Exception:
  49. pass
  50. raise e
  51. def handle_uploaded_file(user, uploaded_file):
  52. image = validate_uploaded_file(uploaded_file)
  53. store.store_temporary_avatar(user, image)
  54. def clean_crop(image, crop):
  55. message = _("Crop data is invalid. Please try again.")
  56. crop_dict = {}
  57. try:
  58. crop_dict = {
  59. 'x': float(crop['offset']['x']),
  60. 'y': float(crop['offset']['y']),
  61. 'zoom': float(crop['zoom']),
  62. }
  63. except (KeyError, TypeError, ValueError):
  64. raise ValidationError(message)
  65. if crop_dict['zoom'] < 0 or crop_dict['zoom'] > 1:
  66. raise ValidationError(message)
  67. min_size = max(settings.MISAGO_AVATARS_SIZES)
  68. zoomed_size = (
  69. round(float(image.size[0]) * crop_dict['zoom'], 2),
  70. round(float(image.size[1]) * crop_dict['zoom'], 2)
  71. )
  72. if min(zoomed_size) < min_size:
  73. raise ValidationError(message)
  74. crop_square = {
  75. 'x': crop_dict['x'] * -1,
  76. 'y': crop_dict['y'] * -1,
  77. }
  78. if crop_square['x'] < 0 or crop_square['y'] < 0:
  79. raise ValidationError(message)
  80. if crop_square['x'] + min_size > zoomed_size[0]:
  81. raise ValidationError(message)
  82. if crop_square['y'] + min_size > zoomed_size[1]:
  83. raise ValidationError(message)
  84. return crop_dict
  85. def crop_source_image(user, source, crop):
  86. if source == 'tmp':
  87. image = Image.open(user.avatar_tmp)
  88. else:
  89. image = Image.open(user.avatar_src)
  90. crop = clean_crop(image, crop)
  91. min_size = max(settings.MISAGO_AVATARS_SIZES)
  92. if image.size[0] == min_size and image.size[0] == image.size[1]:
  93. cropped_image = image
  94. else:
  95. upscale = 1.0 / crop['zoom']
  96. cropped_image = image.crop((
  97. int(round(crop['x'] * upscale * -1, 0)), int(round(crop['y'] * upscale * -1, 0)),
  98. int(round((crop['x'] - min_size) * upscale * -1, 0)),
  99. int(round((crop['y'] - min_size) * upscale * -1, 0)),
  100. ))
  101. if source == 'tmp':
  102. store.store_new_avatar(user, cropped_image, delete_tmp=False)
  103. store.store_original_avatar(user)
  104. else:
  105. store.store_new_avatar(user, cropped_image, delete_src=False)
  106. return crop
  107. def has_temporary_avatar(user):
  108. return bool(user.avatar_tmp)
  109. def has_source_avatar(user):
  110. return bool(user.avatar_src)