diff --git a/cypress/fixtures/sample_image.jpg b/cypress/fixtures/sample_image.jpg
new file mode 100644
index 0000000000..6322b65e33
Binary files /dev/null and b/cypress/fixtures/sample_image.jpg differ
diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js
index 2f457983de..e1e232c058 100644
--- a/cypress/integration/file_uploader.js
+++ b/cypress/integration/file_uploader.js
@@ -54,4 +54,24 @@ context('FileUploader', () => {
.should('have.property', 'file_url', 'https://github.com');
cy.get('.modal:visible').should('not.exist');
});
+
+ it('should allow cropping and optimization for valid images', () => {
+ open_upload_dialog();
+
+ cy.get_open_dialog().find('.file-upload-area').attachFile('sample_image.jpg', {
+ subjectType: 'drag-n-drop',
+ });
+
+ cy.get_open_dialog().find('.file-name').should('contain', 'sample_image.jpg');
+ cy.get_open_dialog().find('.btn-crop').first().click();
+ cy.get_open_dialog().find('.image-cropper-actions > .btn-primary').should('contain', 'Crop');
+ cy.get_open_dialog().find('.image-cropper-actions > .btn-primary').click();
+ cy.get_open_dialog().find('.optimize-checkbox').first().should('contain', 'Optimize');
+ cy.get_open_dialog().find('.optimize-checkbox').first().click();
+
+ cy.intercept('POST', '/api/method/upload_file').as('upload_file');
+ cy.get_open_dialog().find('.btn-modal-primary').click();
+ cy.wait('@upload_file').its('response.statusCode').should('eq', 200);
+ cy.get('.modal:visible').should('not.exist');
+ });
});
diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js
index 6d77cb91ad..bc0cc17553 100644
--- a/frappe/core/doctype/file/file.js
+++ b/frappe/core/doctype/file/file.js
@@ -23,6 +23,25 @@ frappe.ui.form.on("File", "refresh", function(frm) {
wrapper.empty();
}
+ var is_raster_image = (/\.(gif|jpg|jpeg|tiff|png)$/i).test(frm.doc.file_url);
+ var is_optimizable = !frm.doc.is_folder && is_raster_image && frm.doc.file_size > 0;
+
+ if (is_optimizable) {
+ frm.add_custom_button(__("Optimize"), function() {
+ frappe.show_alert(__("Optimizing image..."));
+ frappe.call({
+ method: "frappe.core.doctype.file.file.optimize_saved_image",
+ args: {
+ doc_name: frm.doc.name,
+ },
+ callback: function() {
+ frappe.show_alert(__("Image optimized"));
+ frappe.set_route("List", "File");
+ }
+ });
+ });
+ }
+
if(frm.doc.file_name && frm.doc.file_name.split('.').splice(-1)[0]==='zip') {
frm.add_custom_button(__('Unzip'), function() {
frappe.call({
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 95c33879e6..e79b2bd761 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -28,7 +28,7 @@ import frappe
from frappe import _, conf
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
-from frappe.utils.image import strip_exif_data
+from frappe.utils.image import strip_exif_data, optimize_image
class MaxFileSizeReachedError(frappe.ValidationError):
pass
@@ -879,6 +879,15 @@ def extract_images_from_html(doc, content):
data = match.group(1)
data = data.split("data:")[1]
headers, content = data.split(",")
+ mtype = headers.split(";")[0]
+
+ if isinstance(content, str):
+ content = content.encode("utf-8")
+ if b"," in content:
+ content = content.split(b",")[1]
+ content = base64.b64decode(content)
+
+ content = optimize_image(content, mtype)
if "filename=" in headers:
filename = headers.split("filename=")[-1]
@@ -887,7 +896,6 @@ def extract_images_from_html(doc, content):
if not isinstance(filename, str):
filename = str(filename, 'utf-8')
else:
- mtype = headers.split(";")[0]
filename = get_random_filename(content_type=mtype)
doctype = doc.parenttype if doc.parent else doc.doctype
@@ -899,7 +907,7 @@ def extract_images_from_html(doc, content):
"attached_to_doctype": doctype,
"attached_to_name": name,
"content": content,
- "decode": True
+ "decode": False
})
_file.save(ignore_permissions=True)
file_url = _file.file_url
@@ -932,6 +940,22 @@ def unzip_file(name):
files = file_obj.unzip()
return len(files)
+@frappe.whitelist()
+def optimize_saved_image(doc_name):
+ file_doc = frappe.get_doc('File', doc_name)
+ content = file_doc.get_content()
+ content_type = mimetypes.guess_type(file_doc.file_name)[0]
+
+ optimized_content = optimize_image(content, content_type)
+
+ file_path = get_files_path(is_private=file_doc.is_private)
+ file_path = os.path.join(file_path.encode('utf-8'), file_doc.file_name.encode('utf-8'))
+ with open(file_path, 'wb+') as f:
+ f.write(optimized_content)
+
+ file_doc.file_size = len(optimized_content)
+ file_doc.content_hash = get_content_hash(optimized_content)
+ file_doc.save()
@frappe.whitelist()
def get_attached_images(doctype, names):
diff --git a/frappe/handler.py b/frappe/handler.py
index 9d32b0acee..2e9fb7b454 100755
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -10,6 +10,8 @@ from frappe.utils import cint
from frappe import _, is_whitelisted
from frappe.utils.response import build_response
from frappe.utils.csvutils import build_csv_response
+from frappe.utils.image import optimize_image
+from mimetypes import guess_type
from frappe.core.doctype.server_script.server_script_utils import run_server_script_api
@@ -145,6 +147,7 @@ def upload_file():
folder = frappe.form_dict.folder or 'Home'
method = frappe.form_dict.method
filename = frappe.form_dict.file_name
+ optimize = frappe.form_dict.optimize
content = None
if 'file' in files:
@@ -152,12 +155,23 @@ def upload_file():
content = file.stream.read()
filename = file.filename
+ content_type = guess_type(filename)[0]
+ if optimize and content_type.startswith("image/"):
+ args = {
+ "content": content,
+ "content_type": content_type
+ }
+ if frappe.form_dict.max_width:
+ args["max_width"] = int(frappe.form_dict.max_width)
+ if frappe.form_dict.max_height:
+ args["max_height"] = int(frappe.form_dict.max_height)
+ content = optimize_image(**args)
+
frappe.local.uploaded_file = content
frappe.local.uploaded_filename = filename
if not file_url and (frappe.session.user == "Guest" or (user and not user.has_desk_access())):
- import mimetypes
- filetype = mimetypes.guess_type(filename)[0]
+ filetype = guess_type(filename)[0]
if filetype not in ALLOWED_MIMETYPES:
frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents."))
diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg
index 7845811627..6108daa938 100644
--- a/frappe/public/icons/timeless/symbol-defs.svg
+++ b/frappe/public/icons/timeless/symbol-defs.svg
@@ -699,4 +699,7 @@