Created
February 22, 2015 09:43
-
-
Save neara/da2b499a2d94204aee3d to your computer and use it in GitHub Desktop.
jQuery resize image plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| $ = jQuery | |
| # FIXME add support for resize without preview | |
| $.fn.extend | |
| # plugin name | |
| resizeImage: (options) -> | |
| # default settings | |
| settings = | |
| crop: false | |
| cropWidth: 200 # in px | |
| cropHeight: 200 # in px | |
| cropWindowX: 10 # in px, crop window start position | |
| cropWindowY: 10 # in px, crop window start position | |
| cropPreview: null # img element to show crop result | |
| cropBtn: '#cropBtn' # crops from the window and shows in cropPreview element | |
| showPreview: true | |
| imgTypes: /^(image\/jpeg|image\/png)$/i # allowed image types | |
| maxSize: 20971520 # 20MB | |
| maxWidth: 1024 # in px | |
| maxHeight: 1024 # in px | |
| form: $(@).parents('form') | |
| previewContainer: null # will center the image inside this container | |
| previewLabelId: 'imgPreviewLabel' | |
| typeError: -> | |
| sizeError: -> | |
| # merge default settings with options | |
| settings = $.extend settings, options | |
| if settings.showPreview and not settings.imgPreview | |
| throw 'Missing image preview elements' | |
| cropWindow = | |
| x: settings.cropWindowX | |
| y: settings.cropWindowY | |
| w: settings.cropWidth | |
| h: settings.cropHeight | |
| # extra vars to dragging calculations | |
| px: settings.cropWindowX | |
| py: settings.cropWindowY | |
| ratio: 1 | |
| dragWhole: false # tracks when user is moving the crop window | |
| # resize corners details | |
| cornerSize: 6 | |
| cornerHoverSize: 10 | |
| cornersSizes: [6, 6, 6, 6] | |
| cornersHoverState: [false, false, false, false] | |
| cornersDragState: [false, false, false, false] | |
| # tracks mouse movement over the image | |
| iMouseX = 1 | |
| iMouseY = 1 | |
| originXCropWindow = settings.cropWindowX | |
| originYCropWindow = settings.cropWindowY | |
| form = $(settings.form)[0] | |
| $imgPreview = $(settings.imgPreview) | |
| # setting an id on label and preview element in order to center the image using blImage | |
| if settings.previewContainer and $(settings.previewContainer).attr('id') | |
| settings.previewLabelId = $(settings.previewContainer).attr('id') | |
| else | |
| $imgPreview.parents('label').attr 'id', 'imgPreviewLabel' | |
| imgPreviewId = $imgPreview.attr('id') | |
| if not imgPreviewId | |
| $imgPreview.attr 'id', 'imgPreview' | |
| imgPreviewId = 'imgPreview' | |
| imgPreview = $imgPreview[0] | |
| ############ Methods ########################## | |
| calculateRatio = (image, maxWidth, maxHeight, ratio=1) -> | |
| """ | |
| Recursively caclculate ratio for the image | |
| 1. Find what is bigger: image.width or image.height | |
| 2. Find if either image.width or image.height overflows maxWidth/maxHeigh | |
| 3. Calculate ratio based on biggest side (image.width or image.height) | |
| 4. Check if new image.width/image.height still overflows maxWidth/maxHeight | |
| 4.1 if there is still an overflow, recalculate ratio based on ratio from step 3 | |
| 4.2 both width and height fit into maxWidth/maxHeight, return current ratio | |
| `Special Case - CUBE` - image.width == image.height | |
| Ratio will be calculated based on image.width, and recalculated as needed | |
| `Special Case - image smaller than maxWidth/Height` | |
| Return ratio will be 1, no calculations will be made. | |
| """ | |
| if image.width * ratio > maxWidth | |
| ratio *= maxWidth / (image.width * ratio) | |
| if image.height * ratio > maxHeight | |
| ratio *= maxHeight / (image.height * ratio) | |
| if image.width * ratio > maxWidth or image.height * ratio > maxHeight | |
| return calculateRatio(image, maxWidth, maxHeight, ratio) | |
| return ratio | |
| detectedIOSDevice = -> | |
| iosDevices = ['iPhone', 'iPad', 'iPod'] | |
| for i in iosDevices | |
| if navigator.platform == i | |
| return true | |
| return false | |
| detectedIOSSubsampling = (img) -> | |
| naturalWidth = img.naturalWidth | |
| naturalHeight = img.naturalHeight | |
| if naturalHeight * naturalWidth > 1024 * 1024 | |
| canvas = document.createElement('canvas') | |
| canvas.width = canvas.height = 1 | |
| ctx = canvas.getContext('2d') | |
| ctx.drawImage(img, -naturalWidth + 1, 0) | |
| return ctx.getImageData(0, 0, 1, 1).data[3] == 0 | |
| return false | |
| detectVerticalSquash = (img, height) -> | |
| canvas = document.createElement('canvas') | |
| canvas.width = 1 | |
| canvas.height = height | |
| ctx = canvas.getContext('2d') | |
| ctx.drawImage(img, 0, 0) | |
| data = ctx.getImageData(0, 0, 1, height).data | |
| startY = 0 | |
| endY = height | |
| pointerY = height | |
| while pointerY > startY | |
| alpha = data[(pointerY - 1) * 4 + 3] | |
| if alpha == 0 | |
| endY = pointerY | |
| else | |
| startY = pointerY | |
| pointerY = (endY + startY) >> 1 # bynary right shift propagation by 1 bit | |
| ratio = (pointerY / height) | |
| return if ratio == 0 then 1 else ratio | |
| resizeImage = (urlToLocalImage, MAX_WIDTH=settings.maxWidth, MAX_HEIGHT=settings.maxHeight) -> | |
| ### | |
| This is a helper function for reducing uploaded image file size | |
| via dimensions (width/height) change. Returns new url to changed image in | |
| jpeg format. Returned url is of the datauri format: `data:<mimetype>;charset=<charset>;base64, ...` | |
| charset is optional | |
| `urlToLocalImage` - url to image in browser buffer | |
| `MAX_WIDTH` - (optional) desired image width, default is 1024px | |
| `MAX_HEIGHT` - (optional) desired image height, default is 1024px | |
| ### | |
| maxWidth = MAX_WIDTH | |
| maxHeight = MAX_HEIGHT | |
| img = new Image() | |
| img.src = urlToLocalImage | |
| # calculate ratio | |
| ratio = 1 | |
| if detectedIOSDevice() | |
| ratio = detectVerticalSquash(img, img.naturalHeight) | |
| else | |
| ratio = calculateRatio(img, maxWidth, maxHeight) | |
| factor = 1 | |
| if detectedIOSSubsampling(img) | |
| factor = 2 | |
| canvas = document.createElement('canvas') | |
| canvas.width = Math.ceil(img.width * ratio) | |
| canvas.height = Math.ceil(img.height * ratio) | |
| ctx = canvas.getContext('2d') | |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height * factor) | |
| dataUrl = canvas.toDataURL('image/jpeg', 0.7) | |
| return dataUrl | |
| appendResizedImgToForm = (url, imgName) -> | |
| ### | |
| Appends or updates resized image and image file name to the statusForm | |
| Those fields will be uploaded to server | |
| ### | |
| $imgInput = $('input[name=image]') | |
| if $imgInput.length == 0 | |
| resizedImg = document.createElement('input') | |
| resizedImg.type = 'hidden' | |
| resizedImg.name = 'image' | |
| resizedImg.value = url | |
| form.appendChild(resizedImg) | |
| else | |
| $imgInput.val(url) | |
| $imgFileName = $('input[name=imageFileName]') | |
| if $imgFileName.length == 0 | |
| imgFileName = document.createElement('input') | |
| imgFileName.type = 'hidden' | |
| imgFileName.name = 'imageFileName' | |
| imgFileName.value = imgName | |
| form.appendChild(imgFileName) | |
| else | |
| $imgFileName.val(imgName) | |
| handleResize = (imgURL, imageName) -> | |
| # this function will be fired when any read is done on the file object | |
| # e.target.result contains the DataURL which i will use as a source of the imageURL | |
| url = resizeImage(imgURL) | |
| imgPreview.onload = (event) -> | |
| $("\##{settings.previewLabelId} \##{imgPreviewId}").centerImage('inside') | |
| imgPreview.src = url | |
| appendResizedImgToForm(url, settings.fileName) | |
| drawCropPreview = (image, r=cropWindow.ratio) -> | |
| tmp_canvas = document.createElement('canvas') | |
| tmp_canvas.width = cropWindow.w | |
| tmp_canvas.height = cropWindow.h | |
| tmp_ctx = tmp_canvas.getContext('2d') | |
| tmp_ctx.drawImage(image, | |
| cropWindow.x / r, cropWindow.y / r, cropWindow.w / r, cropWindow.h / r, | |
| 0, 0, cropWindow.w, cropWindow.h | |
| ) | |
| croppedImgUrl = tmp_canvas.toDataURL('image/jpeg', 0.7) | |
| $(settings.cropPreview).attr('src', croppedImgUrl) | |
| appendResizedImgToForm(croppedImgUrl, settings.fileName) | |
| drawSelectionPanel = (ctx, image) -> | |
| ctx.strokeStyle = '#000' | |
| ctx.lineWidth = 2 | |
| ctx.strokeRect(cropWindow.x, cropWindow.y, cropWindow.w, cropWindow.h) | |
| # draw the image in crop window | |
| if cropWindow.w > 0 and cropWindow.h > 0 | |
| r = cropWindow.ratio | |
| ctx.drawImage(image, | |
| cropWindow.x / r, cropWindow.y / r, cropWindow.w / r, cropWindow.h / r, | |
| cropWindow.x, cropWindow.y, cropWindow.w, cropWindow.h) | |
| # draw resize corners | |
| ctx.fillStyle = '#fff' | |
| ctx.fillRect( | |
| cropWindow.x - cropWindow.cornersSizes[0], | |
| cropWindow.y - cropWindow.cornersSizes[0], | |
| cropWindow.cornersSizes[0] * 2, | |
| cropWindow.cornersSizes[0] * 2 | |
| ) | |
| ctx.fillRect( | |
| cropWindow.x + cropWindow.w - cropWindow.cornersSizes[1], | |
| cropWindow.y - cropWindow.cornersSizes[1], | |
| cropWindow.cornersSizes[1] * 2, | |
| cropWindow.cornersSizes[1] * 2 | |
| ) | |
| ctx.fillRect( | |
| cropWindow.x + cropWindow.w - cropWindow.cornersSizes[2], | |
| cropWindow.y + cropWindow.h - cropWindow.cornersSizes[2], | |
| cropWindow.cornersSizes[2] * 2, | |
| cropWindow.cornersSizes[2] * 2 | |
| ) | |
| ctx.fillRect( | |
| cropWindow.x - cropWindow.cornersSizes[3], | |
| cropWindow.y + cropWindow.h - cropWindow.cornersSizes[3], | |
| cropWindow.cornersSizes[3] * 2, | |
| cropWindow.cornersSizes[3] * 2 | |
| ) | |
| drawScene = (ctx, image) -> | |
| # clear canvas, prepare to draw the image with crop window | |
| ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) | |
| # draw the image on canvas | |
| ctx.drawImage(image, 0, 0, ctx.canvas.width, ctx.canvas.height) | |
| # darken the image to highlight the window | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.7)' | |
| ctx.fillRect 0, 0, ctx.canvas.width, ctx.canvas.height | |
| drawSelectionPanel(ctx, image) | |
| handleCrop = (imgURL) -> | |
| $previewContainer = $(settings.previewContainer) | |
| cropWindow.x = originXCropWindow | |
| cropWindow.y = originYCropWindow | |
| image = new Image() | |
| image.onload = (event) -> | |
| cropWindow.ratio = calculateRatio(image, $previewContainer.width(), $previewContainer.height()) | |
| if settings.crop and settings.cropPreview | |
| $(settings.cropBtn).click (event) -> | |
| event.preventDefault() | |
| drawCropPreview(image) | |
| canvas = document.createElement('canvas') | |
| canvas.width = Math.ceil(image.width * cropWindow.ratio) | |
| canvas.height = Math.ceil(image.height * cropWindow.ratio) | |
| canvas.id = imgPreviewId | |
| ctx = canvas.getContext('2d') | |
| if canvas.width < cropWindow.w + cropWindow.x | |
| cropWindow.w = canvas.width - cropWindow.x * 2 | |
| if canvas.height < cropWindow.h + cropWindow.y | |
| cropWindow.h = canvas.height - cropWindow.y * 2 | |
| drawScene(ctx, image) | |
| $previewContainer.empty() | |
| $previewContainer.append(canvas) | |
| $("\##{imgPreviewId}") | |
| .mousemove (event) -> | |
| draw = false | |
| canvasOffset = $("\##{imgPreviewId}").offset() | |
| iMouseX = Math.floor(event.pageX - canvasOffset.left) | |
| iMouseY = Math.floor(event.pageY - canvasOffset.top) | |
| if cropWindow.dragWhole | |
| cropWindow.x = iMouseX - cropWindow.px | |
| cropWindow.y = iMouseY - cropWindow.py | |
| draw = true | |
| # reset resize corners state | |
| for i in [0...cropWindow.cornersSizes.length] | |
| cropWindow.cornersHoverState[i] = false | |
| cropWindow.cornersSizes[i] = cropWindow.cornerSize | |
| # detect hover over resize corners | |
| if iMouseX > cropWindow.x - cropWindow.cornerHoverSize and | |
| iMouseX < cropWindow.x + cropWindow.cornerHoverSize and | |
| iMouseY > cropWindow.y - cropWindow.cornerHoverSize and | |
| iMouseY < cropWindow.y + cropWindow.cornerHoverSize | |
| draw = true | |
| cropWindow.cornersHoverState[0] = true | |
| cropWindow.cornersSizes[0] = cropWindow.cornerHoverSize | |
| if iMouseX > cropWindow.x + cropWindow.w - cropWindow.cornerHoverSize and | |
| iMouseX < cropWindow.x + cropWindow.w + cropWindow.cornerHoverSize and | |
| iMouseY > cropWindow.y - cropWindow.cornerHoverSize and | |
| iMouseY < cropWindow.y + cropWindow.cornerHoverSize | |
| draw = true | |
| cropWindow.cornersHoverState[1] = true; | |
| cropWindow.cornersSizes[1] = cropWindow.cornerHoverSize | |
| if iMouseX > cropWindow.x + cropWindow.w - cropWindow.cornerHoverSize and | |
| iMouseX < cropWindow.x + cropWindow.w + cropWindow.cornerHoverSize and | |
| iMouseY > cropWindow.y + cropWindow.h - cropWindow.cornerHoverSize and | |
| iMouseY < cropWindow.y + cropWindow.h + cropWindow.cornerHoverSize | |
| draw = true | |
| cropWindow.cornersHoverState[2] = true | |
| cropWindow.cornersSizes[2] = cropWindow.cornerHoverSize | |
| if iMouseX > cropWindow.x - cropWindow.cornerHoverSize and | |
| iMouseX < cropWindow.x + cropWindow.cornerHoverSize and | |
| iMouseY > cropWindow.y + cropWindow.h-cropWindow.cornerHoverSize and | |
| iMouseY < cropWindow.y + cropWindow.h + cropWindow.cornerHoverSize | |
| draw = true | |
| cropWindow.cornersHoverState[3] = true | |
| cropWindow.cornersSizes[3] = cropWindow.cornerHoverSize | |
| # in case of dragging of resize cubes | |
| # iFX - new crop window X position | |
| # iFY - new crop window Y position | |
| # iFW - new crop window width | |
| # iFH - new crop window height | |
| if cropWindow.cornersDragState[0] | |
| # user dragged left upper corner | |
| iFX = iMouseX - cropWindow.px | |
| iFY = iMouseY - cropWindow.py | |
| iFW = cropWindow.w + cropWindow.x - iFX | |
| iFH = cropWindow.h + cropWindow.y - iFY | |
| if cropWindow.cornersDragState[1] | |
| # user dragged right upper corner | |
| iFX = cropWindow.x | |
| iFY = iMouseY - cropWindow.py | |
| iFW = iMouseX - cropWindow.px - iFX | |
| iFH = cropWindow.h + cropWindow.y - iFY | |
| if cropWindow.cornersDragState[2] | |
| # user dragged right bottom corner | |
| iFX = cropWindow.x | |
| iFY = cropWindow.y | |
| iFW = iMouseX - cropWindow.px - iFX | |
| iFH = iMouseY - cropWindow.py - iFY | |
| if cropWindow.cornersDragState[3] | |
| # user dragged left bottom corner | |
| iFX = iMouseX - cropWindow.px | |
| iFY = cropWindow.y | |
| iFW = cropWindow.w + cropWindow.x - iFX | |
| iFH = iMouseY - cropWindow.py - iFY | |
| if iFW > cropWindow.cornerHoverSize * 2 and iFH > cropWindow.cornerHoverSize * 2 | |
| # user changed crop window size by dragging one of the corners | |
| # update crop window to new size | |
| cropWindow.w = iFW | |
| cropWindow.h = iFH | |
| cropWindow.x = iFX | |
| cropWindow.y = iFY | |
| draw = true | |
| if draw | |
| drawScene(ctx, image) | |
| .mousedown (event) -> | |
| event.stopPropagation() | |
| event.stopImmediatePropagation() | |
| canvasOffset = $("\##{imgPreviewId}").offset() | |
| iMouseX = Math.floor(event.pageX - canvasOffset.left) | |
| iMouseY = Math.floor(event.pageY - canvasOffset.top) | |
| cropWindow.px = iMouseX - cropWindow.x | |
| cropWindow.py = iMouseY - cropWindow.y | |
| # register crop window movement | |
| if iMouseX > cropWindow.x and iMouseX < cropWindow.x + cropWindow.w \ | |
| and iMouseY > cropWindow.y and iMouseY < cropWindow.y + cropWindow.h | |
| cropWindow.dragWhole = true | |
| return | |
| # register resize of crop window | |
| if cropWindow.cornersHoverState[0] | |
| # user is hovering over upper left corner | |
| cropWindow.px = iMouseX - cropWindow.x | |
| cropWindow.py = iMouseY - cropWindow.y | |
| if cropWindow.cornersHoverState[1] | |
| # user is hovering over upper right corner | |
| cropWindow.px = iMouseX - cropWindow.x - cropWindow.w | |
| cropWindow.py = iMouseY - cropWindow.y | |
| if cropWindow.cornersHoverState[2] | |
| # user is hovering over bottom right corner | |
| cropWindow.px = iMouseX - cropWindow.x - cropWindow.w | |
| cropWindow.py = iMouseY - cropWindow.y - cropWindow.h | |
| if cropWindow.cornersHoverState[3] | |
| # user is hovering over bottom left corner | |
| cropWindow.px = iMouseX - cropWindow.x | |
| cropWindow.py = iMouseY - cropWindow.y - cropWindow.h | |
| # register what corner is being dragged | |
| for i in [0...cropWindow.cornersHoverState.length] | |
| if cropWindow.cornersHoverState[i] | |
| cropWindow.cornersDragState[i] = true | |
| return false | |
| .mouseup (event) -> | |
| event.stopPropagation() | |
| event.stopImmediatePropagation() | |
| cropWindow.dragWhole = false | |
| cropWindow.px = 0 | |
| cropWindow.py = 0 | |
| for i in [0...cropWindow.cornersDragState.length] | |
| cropWindow.cornersDragState[i] = false | |
| return false | |
| image.src = imgURL | |
| handleFileInput = (el) -> | |
| ### | |
| Uploads the file from user device into browser buffer. | |
| Checks that selected image is of image mimetype, and doesn't exceeds max | |
| file size allowed. If the image is bigger than the maxFileSize, | |
| upload will be aborted. | |
| Automatically reduce image size, by changing image dimensions. | |
| ### | |
| file = el.files[0] | |
| if not settings.imgTypes.test file.type | |
| console.log 'type not allowed' | |
| settings.typeError() | |
| return false | |
| if file.size > settings.maxSize | |
| console.log 'size too big' | |
| settings.sizeError() | |
| return false | |
| # prepare filereader | |
| fReader = new FileReader() | |
| fReader.onload = (event) => | |
| settings.fileName = file.name | |
| if settings.crop | |
| handleCrop(event.target.result) | |
| else | |
| handleResize(event.target.result) | |
| # trigger onload func | |
| fReader.readAsDataURL(file) | |
| return @.change -> | |
| handleFileInput(@) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment