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