uploaded.py 4.5 KB

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