1 // The Antville Project
  2 // http://code.google.com/p/antville
  3 //
  4 // Copyright 2007-2011 by Tobi Schäfer.
  5 //
  6 // Copyright 2001–2007 Robert Gaggl, Hannes Wallnöfer, Tobi Schäfer,
  7 // Matthias & Michael Platzer, Christoph Lincke.
  8 //
  9 // Licensed under the Apache License, Version 2.0 (the ``License'');
 10 // you may not use this file except in compliance with the License.
 11 // You may obtain a copy of the License at
 12 //
 13 //    http://www.apache.org/licenses/LICENSE-2.0
 14 //
 15 // Unless required by applicable law or agreed to in writing, software
 16 // distributed under the License is distributed on an ``AS IS'' BASIS,
 17 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 18 // See the License for the specific language governing permissions and
 19 // limitations under the License.
 20 //
 21 // $Revision$
 22 // $Author$
 23 // $Date$
 24 // $URL$
 25 
 26 /**
 27  * @fileOverview Defines the Image prototype.
 28  */
 29 
 30 markgettext("Image");
 31 markgettext("image");
 32 
 33 this.handleMetadata("contentLength");
 34 this.handleMetadata("contentType");
 35 this.handleMetadata("description");
 36 this.handleMetadata("fileName");
 37 this.handleMetadata("height");
 38 this.handleMetadata("thumbnailHeight");
 39 this.handleMetadata("thumbnailName");
 40 this.handleMetadata("thumbnailWidth");
 41 this.handleMetadata("origin");
 42 this.handleMetadata("width");
 43 
 44 /** @constant */
 45 Image.THUMBNAILWIDTH = 100;
 46 
 47 /** @constant */
 48 Image.KEYS = ["name", "created", "modified", "origin", "description", 
 49       "contentType", "contentLength", "width", "height", "thumbnailName", 
 50       "thumbnailWidth", "thumbnailHeight", "fileName", "site"];
 51 
 52 /**
 53  * @param {Object} data
 54  * @param {Site|Layout} parent
 55  * @param {User} user
 56  * @returns {Image}
 57  */
 58 Image.add = function(data, parent, user) {
 59    HopObject.confirmConstructor(Image);
 60    parent || (parent = res.handlers.site);
 61    user || (user = session.user);
 62    var image = new Image;
 63    if (data) {
 64       for each (var key in Image.KEYS) {
 65          image[key] = data[key];
 66       }
 67    }
 68    image.parent = parent;
 69    image.created = image.modified = new Date;
 70    image.creator = image.modifier = user;
 71    image.update(data);
 72    parent.images.add(image);
 73    return image;
 74 }
 75 
 76 /**
 77  * 
 78  */
 79 Image.remove = function() {
 80    if (this.constructor === Image) {
 81       this.removeFiles();
 82       this.setTags(null);
 83       this.deleteMetadata();
 84       this.remove();
 85    }
 86    return;
 87 }
 88 
 89 /**
 90  * 
 91  * @param {String} type
 92  * @returns {String}
 93  */
 94 Image.getFileExtension = function(type) {
 95    type = String(type);
 96    // Sometimes type is like "image/jpeg;charset=ISO-8859-1"
 97    var index = type.lastIndexOf(";");
 98    if (index > -1) {
 99       type = type.substr(0, index);
100    }
101    switch (type) {
102       //case "image/x-icon":
103       //return ".ico";
104       case "image/gif":
105       return ".gif";
106       case "image/jpeg":
107       case "image/pjpeg":
108       return ".jpg";
109       case "image/png":
110       case "image/x-png":
111       return ".png";
112    }
113    return null;
114 }
115 
116 /**
117  * @name Image
118  * @constructor
119  * @param {Object} data
120  * @property {Number} contentLength
121  * @property {String} contentType
122  * @property {Date} created
123  * @property {User} creator
124  * @property {String} description
125  * @property {String} fileName
126  * @property {Number} height
127  * @property {Metadata} metadata
128  * @property {Date} modified
129  * @property {User} modifier
130  * @property {String} name
131  * @property {} origin
132  * @property {HopObject} parent
133  * @property {Number} parent_id
134  * @property {String} parent_type
135  * @property {String} prototype
136  * @property {Tag[]} tags 
137  * @property {Number} thumbnailHeight
138  * @property {String} thumbnailName
139  * @property {Number} thumbnailWidth
140  * @property {Number} width
141  * @extends HopObject
142  */
143 Image.prototype.constructor = function(data) {
144    HopObject.confirmConstructor.call(this);
145    return this;
146 }
147 
148 /**
149  * 
150  * @param {String} action
151  * @return {Boolean}
152  */
153 Image.prototype.getPermission = function(action) {
154    var defaultGrant = this._parent.getPermission("main");
155    switch (action) {
156       case ".":
157       case "main":
158       return true;
159       case "delete":
160       return defaultGrant && this.creator === session.user || 
161             Membership.require(Membership.MANAGER) ||
162             User.require(User.PRIVILEGED);            
163       case "edit":
164       return defaultGrant && this.creator === session.user || 
165             Membership.require(Membership.MANAGER) || 
166             User.require(User.PRIVILEGED) &&
167             this.parent_type !== "Layout" ||
168             this.parent === path.layout;
169    }
170    return false;
171 }
172 
173 /**
174  * 
175  * @param {String} action
176  * @returns {String}
177  */
178 Image.prototype.href = function(action) {
179    if (action !== "replace") {
180       if (this.parent_type === "Layout" && this.parent !== path.layout) {
181          return this.getUrl();
182       }
183    } else {
184       return res.handlers.images.href("create") + "?name=" + this.name;
185    }
186    return HopObject.prototype.href.apply(this, arguments);
187 }
188 
189 Image.prototype.main_action = function() {
190    res.data.title = gettext("Image: {0}", this.getTitle());
191    res.data.body = this.renderSkinAsString("Image#main");
192    res.handlers.site.renderSkin("Site#page");
193    return;
194 }
195 
196 Image.prototype.edit_action = function() {
197    File.redirectOnUploadError(this.href(req.action));
198 
199    if (req.postParams.save) {
200       try {
201          File.redirectOnExceededQuota(this.href(req.action));
202          this.update(req.postParams);
203          res.message = gettext("The changes were saved successfully.");
204          res.redirect(this.href());
205       } catch (ex) {
206          res.message = ex;
207          app.log(ex);
208       }
209    }
210 
211    res.data.action = this.href(req.action);
212    res.data.title = gettext("Edit Image");
213    res.data.body = this.renderSkinAsString("$Image#edit");
214    res.handlers.site.renderSkin("Site#page");
215    return;
216 }
217 
218 /**
219  * 
220  * @param {String} name
221  * @returns {Object}
222  */
223 Image.prototype.getFormValue = function(name) {
224    var self = this;
225    
226    var getOrigin = function(str) {
227       var origin = req.postParams.file_origin || self.origin;
228       if (origin && origin.contains("://")) {
229          return origin;
230       }
231       return null;
232    }
233    
234    if (req.isPost()) {
235       if (name === "file") {
236          return getOrigin();
237       }
238       return req.postParams[name];
239    }
240    switch (name) {
241       case "file":
242       return getOrigin();
243       case "maxWidth":
244       case "maxHeight":
245       return this[name] || 400;
246       case "tags":
247       return this.getTags();
248    }
249    return this[name] || req.queryParams[name];
250 }
251 
252 /**
253  * 
254  * @param {Object} data
255  */
256 Image.prototype.update = function(data) {
257    var origin = data.file_origin;
258 
259    if (!origin) {
260       if (this.isTransient()) { 
261          throw Error(gettext("There was nothing to upload. Please be sure to choose a file."));
262       }
263    } else if (origin !== this.origin) {
264       var mime = data.file;
265       // Check if mime is not null to allow post requests with no file upload at all
266       if (!mime || mime.contentLength < 1) {
267          mime = getURL(origin);
268          if (!mime) {
269             throw Error(gettext("Could not fetch the image from the given URL."));
270          }
271       }
272 
273       var extension = Image.getFileExtension(mime.contentType);
274       if (!extension) {
275          throw Error(gettext("This does not seem to be a (valid) JPG, PNG or GIF image file."));
276       }
277       
278       this.origin = origin;
279       var mimeName = mime.normalizeFilename(mime.name);
280       this.contentLength = mime.contentLength;
281       this.contentType = mime.contentType;
282    
283       if (!this.name) {
284           var name = File.getName(data.name) || mimeName.split(".")[0];
285           this.name = this.parent.images.getAccessName(name);
286       }
287 
288       var image = this.getConstraint(mime, data.maxWidth, data.maxHeight);
289       this.height = image.height; 
290       this.width = image.width;
291 
292       var thumbnail;
293       if (image.width > Image.THUMBNAILWIDTH) {
294          thumbnail = this.getConstraint(mime, Image.THUMBNAILWIDTH);
295          this.thumbnailWidth = thumbnail.width; 
296          this.thumbnailHeight = thumbnail.height; 
297       } else if (this.isPersistent()) {
298          this.getThumbnailFile().remove();
299          // NOTE: delete operator won't work here due to getter/setter methods
300          this.deleteMetadata("thumbnailName", "thumbnailWidth", "thumbnailHeight");
301       }
302 
303       // Make the image persistent before proceeding with writing files and 
304       // setting tags (also see Helma bug #607)
305       this.isTransient() && this.persist();
306       
307       var fileName = this.name + extension;
308       if (fileName !== this.fileName) {
309          // Remove existing image files if the file name has changed
310          this.removeFiles();
311       }
312       this.fileName = fileName;
313       thumbnail && (this.thumbnailName = this.name + "_small" + extension);
314       this.writeFiles(image.resized || mime, thumbnail && thumbnail.resized);
315       image.resized && (this.contentLength = this.getFile().getLength());
316    }
317 
318    if (this.parent_type !== "Layout") {
319       this.setTags(data.tags || data.tag_array);
320    }
321    this.description = data.description;
322    this.touch();
323    return;
324 }
325 
326 /**
327  * 
328  */
329 Image.prototype.tags_macro = function() {
330    return res.write(this.getFormValue("tags"));
331 }
332 
333 /**
334  * 
335  */
336 Image.prototype.contentLength_macro = function() {
337    return res.write((this.contentLength / 1024).format("###,###") + " KB");
338 }
339 
340 /**
341  * 
342  */
343 Image.prototype.url_macro = function() {
344    return res.write(this.getUrl());
345 }
346 
347 /**
348  * 
349  */
350 Image.prototype.macro_macro = function() {
351    return HopObject.prototype.macro_macro.call(this, null,  
352          this.parent.constructor === Layout ? "layout.image" : "image");
353 }
354 
355 /**
356  * 
357  * @param {Object} param
358  */
359 Image.prototype.thumbnail_macro = function(param) {
360    if (!this.thumbnailName) {
361       return this.render_macro(param);
362    }
363    param.src = this.getUrl(this.getThumbnailFile().getName());
364    param.title || (param.title = encode(this.description));
365    param.alt = encode(param.alt || param.title);
366    param.width = this.thumbnailWidth || String.EMPTY;
367    param.height = this.thumbnailHeight || String.EMPTY;
368    param.border = (param.border = 0);
369    html.tag("img", param);
370    return;
371 }
372 
373 /**
374  * 
375  * @param {Object} param
376  */
377 Image.prototype.render_macro = function(param) {
378    param.src = this.getUrl();
379    param.title || (param.title = encode(this.description));
380    param.alt = encode(param.alt || param.title);
381    param.width || (param.width = this.width);
382    param.height || (param.height = this.height);
383    param.border || (param.border = 0);
384    html.tag("img", param);
385    return;
386 }
387 
388 /**
389  * 
390  * @param {Object} name
391  * @returns {helma.File}
392  * @see Site#getStaticFile
393  */
394 Image.prototype.getFile = function(name) {
395    name || (name = this.fileName);
396    if (this.parent_type === "Layout") {
397       var layout = this.parent || res.handlers.layout;
398       return layout.getFile(name);
399    }
400    var site = this.parent || res.handlers.site;
401    return site.getStaticFile("images/" + name);
402 }
403 
404 /**
405  * 
406  * @param {Object} name
407  * @returns {String}
408  * @see Site#getStaticUrl
409  */
410 Image.prototype.getUrl = function(name) {
411    name || (name = this.fileName);
412    //name = encodeURIComponent(name);
413    if (this.parent_type === "Layout") {
414       var layout = this.parent || res.handlers.layout;
415       res.push();
416       res.write("layout/");
417       res.write(name);
418       return layout.site.getStaticUrl(res.pop());
419    }
420    var site = this.parent || res.handlers.site;
421    return site.getStaticUrl("images/" + name);
422 }
423 
424 /**
425  * @returns {helma.File}
426  */
427 Image.prototype.getThumbnailFile = function() {
428    return this.getFile(this.thumbnailName);
429 }
430 
431 /**
432  * @returns {String}
433  */
434 Image.prototype.getJSON = function() {
435    return {
436       name: this.name,
437       origin: this.origin,
438       description: this.description,
439       contentType: this.contentType,
440       contentLength: this.contentLength,
441       width: this.width,
442       height: this.height,
443       thumbnailName: this.thumbnailName,
444       thumbnailWidth: this.thumbnailWidth,
445       thumbnailHeight: this.thumbnailHeight,
446       created: this.created,
447       creator: this.creator ? this.creator.name : null,
448       modified: this.modified,
449       modifier: this.modifier ? this.modifier.name : null,
450    }.toSource();
451 }
452 
453 /**
454  * 
455  * @param {helma.util.MimePart} mime
456  * @param {Number} maxWidth
457  * @param {Number} maxHeight
458  * @throws {Error}
459  * @returns {Object}
460  */
461 Image.prototype.getConstraint = function(mime, maxWidth, maxHeight) {
462    try {
463       var image = new helma.Image(mime.inputStream);
464       var factorH = 1, factorV = 1;
465       if (maxWidth && image.width > maxWidth) {
466          factorH = maxWidth / image.width;
467       }
468       if (maxHeight && image.height > maxHeight) {
469          factorV = maxHeight / image.height;
470       }
471       if (factorH !== 1 || factorV !== 1) {
472          var width = Math.ceil(image.width * 
473                (factorH < factorV ? factorH : factorV));
474          var height = Math.ceil(image.height * 
475                (factorH < factorV ? factorH : factorV));
476          image.resize(width, height);
477          if (mime.contentType.endsWith("gif")) {
478             image.reduceColors(256);
479          }
480          return {resized: image, width: image.width, height: image.height};
481       }
482       return {width: image.width, height: image.height};
483    } catch (ex) {
484       app.log(ex);
485       throw Error(gettext("Could not resize the image."));
486    }
487 }
488 
489 /**
490  * 
491  * @param {helma.Image|helma.util.MimePart} data
492  * @param {Object} thumbnail
493  * @throws {Error}
494  */
495 Image.prototype.writeFiles = function(data, thumbnail) {
496    if (data) {
497       try {
498          // If data is a MimeObject (ie. has the writeToFile method)
499          // the image was not resized and thus, we directly write it to disk
500          var file = this.getFile();
501          if (data.saveAs) {
502             data.saveAs(file);
503          } else if (data.writeToFile) {
504             data.writeToFile(file.getParent(), file.getName());
505          }
506          if (thumbnail) {
507             thumbnail.saveAs(this.getThumbnailFile());
508          }
509       } catch (ex) {
510          app.log(ex);
511          throw Error(gettext("Could not save the image file on disk."));         
512       }
513    }
514    return;
515 }
516 
517 /**
518  * @throws {Error}
519  */
520 Image.prototype.removeFiles = function() {
521    try {
522       this.getFile().remove();
523       var thumbnail = this.getThumbnailFile();
524       if (thumbnail) {
525          thumbnail.remove();
526       }
527    } catch (ex) {
528       app.log(ex);
529       throw Error(gettext("Could not remove the image file from disk."));
530    }
531    return;
532 }
533 
534 /**
535  * @returns {String}
536  */
537 Image.prototype.getConfirmText = function() {
538    return gettext("You are about to delete the image {0}.", this.name);
539 }
540