Skip to content

Commit

Permalink
enhancement: image crop feature & refactoring upload files
Browse files Browse the repository at this point in the history
  • Loading branch information
wonknu committed Nov 23, 2016
1 parent 75b643b commit 5a0b342
Show file tree
Hide file tree
Showing 16 changed files with 450 additions and 59 deletions.
29 changes: 27 additions & 2 deletions docs/types/abe-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,32 @@
###Basic example

```html
{{abe type='image' key='image_key' desc='give some tips' width='100' height='100' alt='html alt' tab='default'}}
{{abe type='image' key='image_key' desc='give some tips' thumbs='250x250,350x350'}}
```

###Parameters
###Parameters

> __type__ = image
>
> __key__ = references key to use into your html
optrionnal parameter

- desc = (String)
- display = (String)
- reload = (Boolean)
- thumbs = (Array[WIDTHxHEIGHT] separated with ",")

###Thumbs

If thumbs is used on your tag you can create thumbnails from the image you upload

####Exemple :

uploading a image named *my_image.jpg* will create a thumbs by default named my_image_thumb.jpg

if the tag contains ```thumbs='250x250,350x350'``` this will also create *my_image_thumb_250x250.jpg* & *my_image_thumb_350x350.jpg*

###Important

thumb are created using https://github.com/jwagner/smartcrop-cli which require to have imagemagick installed on your computer, if you don't a fallback library full JS is used (https://github.com/oliver-moran/jimp) but doesn't crop in a smart way your images
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"helmet": "^3.1.0",
"html-minifier": "^3.1.0",
"https": "^1.0.0",
"jimp": "^0.2.27",
"limax": "^1.4.0",
"mkdirp": "^0.5.1",
"moment": "^2.15.2",
Expand All @@ -75,6 +76,7 @@
"prompt": "^1.0.0",
"qs": "^6.0.1",
"request": "^2.69.0",
"smartcrop-cli": "^1.0.2",
"watch": "^1.0.1",
"which": "^1.2.11",
"xss": "^0.2.10"
Expand Down
1 change: 1 addition & 0 deletions src/cli/cms/editor/handlebars/printInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export default function printInput () {
</div>`
}
else if (params.type.indexOf('image') >= 0){
if(params.thumbs != null) commonParams += `data-size="${params.thumbs}"`
res += `<div class="input-group img-upload">
<div class="input-group-addon image">
<span class="glyphicon glyphicon-picture" aria-hidden="true"></span>
Expand Down
211 changes: 211 additions & 0 deletions src/cli/cms/media/image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import execPromise from 'child-process-promise'
import mkdirp from 'mkdirp'
import fse from 'fs-extra'
import limax from 'limax'
import Jimp from 'jimp'
import path from 'path'
import {Promise} from 'bluebird'

import {
abeExtend,
coreUtils,
cmsData,
config
} from '../../'

export function cropAndSaveFile(imageSize, file, newFile) {
var p = new Promise((resolve) => {
Jimp.read(file).then(function (lenna) {
lenna.crop(0, 0, parseInt(imageSize[0]), parseInt(imageSize[1])).write(newFile)
}).catch(function (err) {
console.error(err)
})
resolve()
})
return p
}

export function smartCropAndSaveFile(imageSize, file, newFile) {
var cmd = `node node_modules/smartcrop-cli/smartcrop-cli.js --width ${parseInt(imageSize[0])} --height ${parseInt(imageSize[1])} ${file} ${newFile}`
var p = execPromise.exec(cmd)
return p
}

export function cropAndSaveFiles(images, file, resp) {
var length = images.length
var cropedImage = 0
resp.thumbs = []
var p = new Promise((resolve) => {
for (var i = 0; i < length; i++) {
var image = images[i]
var ext = path.extname(file)
var newFile = file.replace(ext, `_${images[i]}${ext}`)
resp.thumbs.push({
name: newFile.replace(path.join(config.root, config.publish.url), ''),
size: image
})
smartCropAndSaveFile(image.split('x'), file, newFile)
.then(function (result) {
if(result.stderr) {
cropAndSaveFile(image.split('x'), file, newFile).then(function () {
if(++cropedImage === length) {
resolve(resp)
}
})
}
else if(++cropedImage === length) {
resolve(resp)
}
})
.catch(function (err) {
console.log(err)
})
}
})

return p
}

export function generateThumbnail(file) {
var ext = path.extname(file).toLowerCase()
var thumbFileName = file.replace(ext, `_thumb${ext}`)
var thumbFileNameRelative = thumbFileName.replace(path.join(config.root, config.publish.url), '')
var p = new Promise((resolve) => {
var cropThumb = smartCropAndSaveFile([250, 250], file, thumbFileName)
cropThumb.then(function (result) {
var stderr = result.stderr
if(thumbsList != null) {
thumbsList.push({
originalFile: file.replace(path.join(config.root, config.publish.url), ''),
thumbFile: thumbFileNameRelative
})
}
if(stderr) {
cropAndSaveFile([250, 250], file, thumbFileName).then(function () {
resolve({thumb: thumbFileNameRelative})
})
}
else resolve({thumb: thumbFileNameRelative})
})
})

return p
}

export function saveFile(req) {
var p = new Promise((resolve) => {
var folderFilePath = createMediaFolder(req)
var resp = {success: 1}
var filePath
req.pipe(req.busboy)
req.busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
var ext = path.extname(filename).toLowerCase()
var slug = createMediaSlug(filename, ext)
var hasSentHeader = false

filePath = path.join(folderFilePath, slug)
resp['filePath'] = path.join('/' + config.upload.image, slug)

var returnErr = function (msg) {
hasSentHeader = true
file.resume()
resolve({error: 1, response: msg})
}

file.on('limit', function() {
returnErr('file is too big')
})

var isValid = isValidMedia(mimetype, ext)
if(isValid.error) returnErr(isValid.error)

var fstream = fse.createWriteStream(filePath)
fstream.on('finish', function() {
if(hasSentHeader) return
if(/\.(jpg|png|gif|svg)/.test(filePath)) resp = abeExtend.hooks.instance.trigger('afterSaveImage', resp, req)

var thumbPromise = generateThumbnail(filePath)
thumbPromise.then(function (thumbResp) {
resp.thumbnail = thumbResp.thumb
if(req.query.input.indexOf('data-size') > -1){
var thumbsSizes = cmsData.regex.getAttr(req.query.input, 'data-size').split(',')
cropAndSaveFiles(thumbsSizes, filePath, resp).then(function (resp) {
resolve(resp)
})
}
else resolve(resp)
})
})
file.pipe(fstream)
})
})

return p
}

export function isValidMedia(mimetype, ext) {
var allowedExtensions = ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.mp4']
var allowedMimetype = ['image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'video/mp4']
var error = false
if (allowedMimetype.indexOf(mimetype) < 0) error = 'unauthorized file'
else if (allowedExtensions.indexOf(ext) < 0) error = 'not a valid asset'

return {error: error}
}

export function createMediaSlug(filename, ext) {
var filenameNoExt = path.basename(filename, ext)
return limax(filenameNoExt, {separateNumbers: false}) + '-' + coreUtils.random.generateUniqueIdentifier(2) + ext
}

export function createMediaFolder(req) {
var folderWebPath = '/' + config.upload.image
folderWebPath = abeExtend.hooks.instance.trigger('beforeSaveImage', folderWebPath, req)
var folderFilePath = path.join(config.root, config.publish.url, folderWebPath)
mkdirp.sync(folderFilePath)

return folderFilePath
}

var thumbsList

export function getThumbsList() {
if(thumbsList != null) return thumbsList
thumbsList = []
var pathToThumbs = path.join(config.root, config.publish.url, config.upload.image)
var files = coreUtils.file.getFilesSync(pathToThumbs, true)
Array.prototype.forEach.call(files, (pathFile) => {
pathFile = pathFile.replace(path.join(config.root, config.publish.url), '')
if(pathFile.indexOf('_thumb.') > -1){
thumbsList.push({
originalFile: pathFile.replace('_thumb.', '.'),
thumbFile: pathFile
})
}
})

return thumbsList
}

export function getAssociatedImageFileFromThumb(name) {
var rexMatchImageName = /_(thumb|\d+x\d+)/
var originalName = name.replace(rexMatchImageName, '')
var imageList = {
thumbFile: name,
originalFile: originalName,
thumbs: []
}
var pathThumb = name.split('/')
pathThumb.pop()
pathThumb = path.join(config.root, config.publish.url, pathThumb.join('/'))

var files = coreUtils.file.getFilesSync(pathThumb, true)
Array.prototype.forEach.call(files, (pathFile) => {
pathFile = pathFile.replace(path.join(config.root, config.publish.url), '')
if(pathFile !== originalName && pathFile !== name && pathFile.replace(rexMatchImageName, '') === originalName){
imageList.thumbs.push(pathFile)
}
})

return imageList
}
5 changes: 5 additions & 0 deletions src/cli/cms/media/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as image from './image'

export {
image
}
4 changes: 3 additions & 1 deletion src/cli/core/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import * as text from './text'
import locales from './locales'
import * as slug from './slug'
import * as file from './file'
import * as random from './random'

export {
array,
sort,
text,
locales,
slug,
file
file,
random
}
3 changes: 3 additions & 0 deletions src/cli/core/utils/random.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function generateUniqueIdentifier(substring = 0) {
return (((1+Math.random())*0x100000)|0).toString(16).substring(substring)
}
2 changes: 2 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import * as cmsEditor from './cms/editor'
import * as cmsOperations from './cms/operations'
import * as cmsTemplates from './cms/templates'
import * as cmsReference from './cms/reference'
import * as cmsMedia from './cms/media'
import * as coreUtils from './core/utils'
import * as abeExtend from './extend'

Expand All @@ -53,6 +54,7 @@ export {
cmsOperations,
cmsTemplates,
cmsReference,
cmsMedia,
coreUtils,
cmsEditor,
abeExtend,
Expand Down
4 changes: 4 additions & 0 deletions src/server/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
,postReference
,getReference
,getPaginate
,getThumbs
,getImage
} from '../routes'

import {
Expand All @@ -47,6 +49,8 @@ router.get('/abe/save-config', getSaveConfig)
router.get('/abe/unpublish*', getUnpublish)
router.get('/abe/delete*', getDelete)
router.get('/abe/reference/*', getReference)
router.get('/abe/thumbs/*', getThumbs)
router.get('/abe/image/*', getImage)
router.post('/abe/upload/*', postUpload)
router.post('/abe/reference/*', postReference)
router.get('/abe/list-url*', function (req, res, next) {
Expand Down
23 changes: 21 additions & 2 deletions src/server/public/scripts/modules/EditorFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,29 @@ export default class EditorFiles {
}
var input = parentTarget.querySelector('input.image-input')
input.value = resp.filePath
if(resp.thumbs){
var parent = input.parentNode
var id = input.id
Array.prototype.forEach.call(resp.thumbs, (thumb) => {
var thumdID = `${id}_${thumb.size}`
var inputThumbs = parent.querySelector(`[data-id="${thumdID}"]`)
if(inputThumbs != null) inputThumbs.value = thumb.name
else {
var inputThumbs = document.createElement('input')
inputThumbs.classList.add('form-control')
inputThumbs.classList.add('form-abe')
inputThumbs.classList.add('image-input')
inputThumbs.id = thumdID
inputThumbs.setAttribute('data-id', thumdID)
inputThumbs.value = thumb.name
inputThumbs.type = 'hidden'
}
parent.appendChild(inputThumbs)
})
}
input.focus()
input.blur()
// window.inpt = input


var nodes = IframeNode('#page-template', '[data-abe-' + input.id + ']')
Array.prototype.forEach.call(nodes, (node) => {
EditorUtils.formToHtml(node, input)
Expand Down
10 changes: 10 additions & 0 deletions src/server/routes/get-image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {
cmsMedia
} from '../../cli'

var route = function(req, res){
res.set('Content-Type', 'application/json')
res.send(JSON.stringify(cmsMedia.image.getAssociatedImageFileFromThumb(req.query.name)))
}

export default route
10 changes: 10 additions & 0 deletions src/server/routes/get-thumbs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {
cmsMedia
} from '../../cli'

var route = function(req, res){
res.set('Content-Type', 'application/json')
res.send(JSON.stringify({thumbs: cmsMedia.image.getThumbsList()}))
}

export default route

0 comments on commit 5a0b342

Please sign in to comment.