diff --git a/app.js b/app.js index d64441046..37c6e62a4 100644 --- a/app.js +++ b/app.js @@ -62,7 +62,7 @@ module.exports = { app.use(favicon(path.join(__dirname, 'public/img/Softwerkskammer16x16.ico'))); app.use(morgan('combined', {stream: winstonStream})); app.use(cookieParser()); - app.use(bodyparser.urlencoded({extended: true})); + app.use(bodyparser.urlencoded({extended: true, limit: '50mb'})); app.use(compress()); app.use(serveStatic(path.join(__dirname, 'public'), { maxAge: 600 * 1000 })); // ten minutes diff --git a/lib/activityresults/index.js b/lib/activityresults/index.js index f3b1d8f29..fb2699cd3 100644 --- a/lib/activityresults/index.js +++ b/lib/activityresults/index.js @@ -1,5 +1,6 @@ 'use strict'; var beans = require('nconf').get('beans'); +var bodyParser = require("body-parser"); var ActivityResult = beans.get('activityresult'); var activityresultsPersistence = beans.get('activityresultsPersistence'); var activityresultsService = beans.get('activityresultsService'); @@ -58,41 +59,58 @@ app.post('/', function (req, res) { }); }); -app.post("/:activityResultName/upload", function (req, res) { - new Form().parse(req, function (err, fields, files) { - if (!files) { - return res.send(400); +var processUploadRequestWithImageUri = function (imageUri, req, res) { + galleryRepository.getMetadataForImage(imageUri, function (err, metadata) { + var date; + if (metadata && metadata.exif) { + date = metadata.exif.dateTime || + metadata.exif.dateTimeOriginal || + metadata.exif.dateTimeDigitized || + new Date(); + } else { + date = new Date(); } - var image = files.image[0]; - galleryRepository.storeImage(image.path, function (err, imageUri) { + + var newPhoto = { + id: uuid.v4(), + uri: galleryApp.path() + imageUri, + timestamp: date, + uploaded_by: req.user.member.state.id + }; + + activityresultsService.addPhotoToActivityResult(req.params.activityResultName, newPhoto, function (err) { + if (req.header('X-Requested-With') === 'XMLHttpRequest') { + return res.send(200, app.path() + req.params.activityResultName + '/photo/' + newPhoto.id + '/edit'); + } + res.location(app.path() + req.params.activityResultName + '/photo/' + newPhoto.id + '/edit'); + res.send(303); + }); + }); +}; + +app.post("/:activityResultName/upload", bodyParser({limit: '50mb'}), function (req, res) { + if (req.body.photo) { + // DataURL + galleryRepository.storeImageFromDataURL(req.body.photo, function (err, imageUri) { if (err) { throw err; } - galleryRepository.getMetadataForImage(imageUri, function (err, metadata) { - var date; - if (metadata && metadata.exif) { - date = metadata.exif.dateTime || - metadata.exif.dateTimeOriginal || - metadata.exif.dateTimeDigitized || - new Date(); - } else { - date = new Date(); + processUploadRequestWithImageUri(imageUri, req, res); + }); + } else { + new Form().parse(req, function (err, fields, files) { + if (!files) { + return res.send(400); + } + var image = files.image[0]; + galleryRepository.storeImage(image.path, function (err, imageUri) { + if (err) { + throw err; } - - var newPhoto = { - id: uuid.v4(), - uri: galleryApp.path() + imageUri, - timestamp: date, - uploaded_by: req.user.member.state.id - }; - - activityresultsService.addPhotoToActivityResult(req.params.activityResultName, newPhoto, function (err) { - res.location(app.path() + req.params.activityResultName + '/photo/' + newPhoto.id + '/edit'); - res.send(303); - }); + processUploadRequestWithImageUri(imageUri, req, res); }); }); - }); + } }); app.get("/:activityResultName/photo/:photoId/edit", function (req, res) { diff --git a/lib/activityresults/views/get.jade b/lib/activityresults/views/get.jade index 947b6f1d9..d35a06d97 100644 --- a/lib/activityresults/views/get.jade +++ b/lib/activityresults/views/get.jade @@ -46,9 +46,11 @@ block content div button(type="submit").btn.btn-success.pull-right= t('activityresults.submit') span#btn-cancel.btn.btn-warning= t('activityresults.cancel') + input#resizeOnClient(type='checkbox',name='resizeOnClient',checked='checked') + label(for='resizeOnClient')= t('activityresults.resize_on_client') - script. + script. function getPreview(files, callback) { if(!files || !files[0]) return null; @@ -96,6 +98,70 @@ block content span.small(style="color: black") +thumbnailInfos(img) + script(type='text/javascript'). + function resizeImageFromFile(file, MAX_WIDTH, MAX_HEIGHT, callback) { + // Load via FileReader + var fr = new FileReader(); + fr.onload = function(e) { + getDimensions(e.target.result, function(width, height, img) { + var newWidth = width; + var newHeight = height; + var aspectRatio = width/height; + // Image is wider than tall + if(width > MAX_WIDTH && aspectRatio >= 1) { + newWidth = MAX_WIDTH; + newHeight = MAX_WIDTH / aspectRatio + } else if(height > MAX_HEIGHT && aspectRatio <= 1) { + newWidth = MAX_HEIGHT * aspectRatio; + newHeight = MAX_HEIGHT; + } + resizeImage(img, newWidth, newHeight, function (canvas) { + callback(canvas.toDataURL('image/jpeg', 0.8)); + }) + }); + } + fr.readAsDataURL(file); + function resizeImage(imgElement, width, height, callback) { + var cvs = document.createElement("canvas"); + cvs.width = width; + cvs.height = height; + var ctx = cvs.getContext('2d'); + ctx.drawImage(imgElement, 0, 0, width, height); + callback(cvs); + } + function getDimensions(imgData, callback) { + var tmpImg = document.createElement("img"); + tmpImg.onload = function () { + callback(tmpImg.width, tmpImg.height, tmpImg); + } + tmpImg.src = imgData; + } + } + + $(function() { + var $recordForm = $('#recordForm'); + $recordForm.on('submit', function (e) { + if (!$('#resizeOnClient').is(':checked')) { + console.log('Not resizing image on client'); + return; + } + var files = $('#input-file')[0].files; + if(!files || !files[0]) { + return; + } + resizeImageFromFile(files[0], 2560, 2560, function (dataURL) { + $.post($recordForm.attr('action'), { + photo: dataURL + }).success(function (data) { + window.location.href = data; + }).error(function (error) { + alert(error); + }); + }) + e.preventDefault(); + }); + }); + script(type='text/javascript'). var ESC_KEY = 27; jQuery(function() { diff --git a/lib/gallery/galleryrepositoryService.js b/lib/gallery/galleryrepositoryService.js index b0b62980e..1761b83bc 100644 --- a/lib/gallery/galleryrepositoryService.js +++ b/lib/gallery/galleryrepositoryService.js @@ -5,6 +5,8 @@ var logger = require('winston').loggers.get('gallery'); var magick = require('imagemagick'); var uuid = require('node-uuid'); var path = require('path'); +var tmp = require('tmp'); +var dataurl = require('dataurl'); function autoOrient(sourceImagePath, targetPath, callback) { if (logger.debug) { @@ -25,7 +27,7 @@ function scale(sourceImagePath, targetPath, width, height, fn) { if (logger.debug) { logger.debug('Scaling `' + sourceImagePath + '\' to ' + width + 'x' + height + ' into `' + targetPath + '\''); } - magick.convert([sourceImagePath, '-scale', width + '!x' + height + '!', targetPath], function (err, stdout) { + magick.convert([sourceImagePath, '-quality', '75', '-scale', width + '!x' + height + '!', targetPath], function (err, stdout) { if (err) { return fn(err, undefined); } @@ -40,7 +42,7 @@ function scaleWidth(sourceImagePath, targetPath, width, fn) { if (logger.debug) { logger.debug('Scaling `' + sourceImagePath + '\' to ' + width + 'x undefined into `' + targetPath + '\''); } - magick.convert([sourceImagePath, '-scale', width, targetPath], function (err, stdout) { + magick.convert([sourceImagePath, '-quality', '75', '-scale', width, targetPath], function (err, stdout) { if (err) { return fn(err, undefined); } @@ -75,6 +77,24 @@ module.exports = { }); }, + storeImageFromDataURL: function storeImageFromDataURL(dataURL, callback) { + var _this = this; + + var dataPackage = dataurl.parse(dataURL); + if (!dataPackage || !dataPackage.mimetype.match(/image\/(jpg|jpeg)/)) { + return callback(new Error('Not a valid dataURL')); + } + // Write to tmpFile + tmp.file({postfix: '.jpg'}, function (err, path, fd) { + _this.fs().writeFile(path, dataPackage.data, {encoding: dataPackage.encoding}, function (err) { + if (err) { + return callback(err); + } + _this.storeImage(path, callback); + }); + }); + }, + getMetadataForImage: function getMetadataForImage(id, callback) { var persistentImageFilePath = this.directory() + '/' + id; magick.readMetadata(persistentImageFilePath, callback); diff --git a/locales/translation-de.json b/locales/translation-de.json index 338beb34d..8e256402f 100644 --- a/locales/translation-de.json +++ b/locales/translation-de.json @@ -175,7 +175,8 @@ "done": "Fertig", "record_image": "Aufzeichnen", "submit": "Weiter", - "photoTitlePlaceholder": "Was kann man hier sehen?" + "photoTitlePlaceholder": "Was kann man hier sehen?", + "resize_on_client": "Bild auf Gerät skalieren" }, "payment": { "title": "Zahlung", diff --git a/locales/translation-en.json b/locales/translation-en.json index 2f1ab843c..6043b7b0b 100644 --- a/locales/translation-en.json +++ b/locales/translation-en.json @@ -174,7 +174,8 @@ "done": "Done", "photoTitlePlaceholder": "What's on this photo?", "record_image": "Record", - "submit": "SUBMIT" + "submit": "SUBMIT", + "resize_on_client": "Resize image on client" }, "payment": { "title": "Payment", diff --git a/package.json b/package.json index 150ede636..5430a9afc 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "cookie-parser": "1.3.2", "CoolBeans": "0.0.9", "csurf": "1.4.0", + "dataurl": "^0.1.0", "express": "4.7.2", "express-session": "1.7.2", "i18next": "1.7.4", @@ -64,6 +65,7 @@ "soap-sympa": "0.0.1", "static-favicon": "1.0.2", "stripe": "2.8.0", + "tmp": "0.0.24", "underscore.string": "2.3.3", "URIjs": "1.13.2", "useragent": "2.0.9", diff --git a/test/activityresults/activityresults_integration_upload_test.js b/test/activityresults/activityresults_integration_upload_test.js index d6e2db550..b761dda48 100644 --- a/test/activityresults/activityresults_integration_upload_test.js +++ b/test/activityresults/activityresults_integration_upload_test.js @@ -45,4 +45,21 @@ describe('/activityresults/:result/upload', function () { .end(done); }); }); + + describe('POST / with dataURL', function () { + it("should store the image via gallery service and redirect to edit", function (done) { + var dataURL = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCADIAMgDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8qqKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/2Q=='; + sinon.stub(galleryRepository, 'storeImage', function (tmpFile, callback) { + callback(null, "my-custom-image-id"); + }); + + //noinspection JSLint + request(createApp(1)) + .post('/foo/upload') + .send({photo: dataURL}) + .expect(303) + .expect('Location', /\/foo\/photo\/[\w+|\-]+\/edit/) + .end(done); + }); + }); }); diff --git a/test/gallery/galleryrepository_test.js b/test/gallery/galleryrepository_test.js index bf6901ff0..de6653f33 100644 --- a/test/gallery/galleryrepository_test.js +++ b/test/gallery/galleryrepository_test.js @@ -48,6 +48,18 @@ describe("the gallery repository on real files", function () { }); }); + it('stores a dataurl image', function (done) { + var dataURL = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCADIAMgDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8qqKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/2Q=='; + service.storeImageFromDataURL(dataURL, function (err, imageId) { + service.retrieveImage(imageId, function (err) { + if (err) { + done(err); + } + done(); + }); + }); + }); + it('provides exif data for a given image', function (done) { var storedImageId = 'exif_image.jpg'; var imagePath = __dirname + '/fixtures/' + storedImageId;