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