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