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