      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