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