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