      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