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