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