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