antville/code/Image/Image.js

624 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// The Antville Project
// http://code.google.com/p/antville
//
// Copyright 20012014 by the Workers of Antville.
//
// Licensed under the Apache License, Version 2.0 (the ``License'');
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an ``AS IS'' BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileOverview Defines the Image prototype.
*/
app.addRepository("modules/helma/Http.js");
markgettext('Image');
markgettext('image');
markgettext('a image // accusative');
this.handleMetadata('contentLength');
this.handleMetadata('contentType');
this.handleMetadata('description');
this.handleMetadata('fileName');
this.handleMetadata('height');
this.handleMetadata('thumbnailHeight');
this.handleMetadata('thumbnailName');
this.handleMetadata('thumbnailWidth');
this.handleMetadata('origin');
this.handleMetadata('width');
/** @constant */
Image.THUMBNAILWIDTH = 100;
/** @constant */
Image.KEYS = ['name', 'created', 'modified', 'origin', 'description',
'contentType', 'contentLength', 'width', 'height', 'thumbnailName',
'thumbnailWidth', 'thumbnailHeight', 'fileName', 'site'];
/**
* @param {Object} data
* @param {Site|Layout} parent
* @param {User} user
* @returns {Image}
*/
Image.add = function(data, parent, user) {
HopObject.confirmConstructor(Image);
parent || (parent = res.handlers.site);
user || (user = session.user);
var image = new Image;
if (data) {
for (let key of Image.KEYS) {
image[key] = data[key];
}
}
image.parent = parent;
image.created = image.modified = new Date;
image.creator = image.modifier = user;
image.update(data);
parent.images.add(image);
return image;
}
/**
*
*/
Image.remove = function() {
if (this.constructor === Image) {
this.removeFiles();
this.setTags(null);
this.deleteMetadata();
this.remove();
}
return;
}
/**
*
* @param {String} type
* @returns {String}
*/
Image.getFileExtension = function(type) {
type = String(type);
// Sometimes type is like 'image/jpeg;charset=ISO-8859-1'
var index = type.lastIndexOf(';');
if (index > -1) {
type = type.substr(0, index);
}
switch (type.toLowerCase()) {
case 'image/gif':
return '.gif';
case 'image/jpg':
case 'image/jpeg':
case 'image/pjpeg':
return '.jpg';
case 'image/png':
case 'image/x-png':
return '.png';
case 'image/webp':
return '.webp';
}
return null;
}
/**
* @name Image
* @constructor
* @param {Object} data
* @property {Number} contentLength
* @property {String} contentType
* @property {Date} created
* @property {User} creator
* @property {String} description
* @property {String} fileName
* @property {Number} height
* @property {Metadata} metadata
* @property {Date} modified
* @property {User} modifier
* @property {String} name
* @property {} origin
* @property {HopObject} parent
* @property {Number} parent_id
* @property {String} parent_type
* @property {String} prototype
* @property {Tag[]} tags
* @property {Number} thumbnailHeight
* @property {String} thumbnailName
* @property {Number} thumbnailWidth
* @property {Number} width
* @extends HopObject
*/
Image.prototype.constructor = function(data) {
HopObject.confirmConstructor.call(this);
return this;
}
/**
*
* @param {String} action
* @return {Boolean}
*/
Image.prototype.getPermission = function(action) {
var defaultGrant = this._parent.getPermission('main');
switch (action) {
case '.':
case 'main':
return true;
case 'delete':
return defaultGrant && this.creator === session.user ||
Membership.require(Membership.MANAGER) ||
User.require(User.PRIVILEGED);
case 'edit':
return defaultGrant && this.creator === session.user ||
Membership.require(Membership.MANAGER) ||
User.require(User.PRIVILEGED) &&
this.parent_type !== 'Layout' ||
this.parent === path.layout;
}
return false;
}
/**
*
* @param {String} action
* @returns {String}
*/
Image.prototype.href = function(action) {
if (action !== 'replace') {
if (this.parent_type === 'Layout' && this.parent !== path.layout) {
return this.getUrl();
}
} else {
return res.handlers.images.href('create') + '?name=' + this.name;
}
return HopObject.prototype.href.apply(this, arguments);
}
Image.prototype.main_action = function() {
res.handlers.site.renderPage({
type: 'article',
schema: 'http://schema.org/ImageObject',
title: gettext('Image: {0}', this.getTitle()),
body: this.renderSkinAsString('Image#main'),
images: [this.getUrl()]
});
return;
}
Image.prototype.edit_action = function() {
File.redirectOnUploadError(this.href(req.action));
if (req.postParams.save) {
try {
File.redirectOnExceededQuota(this.href(req.action));
this.update(req.postParams);
res.message = gettext('The changes were saved successfully.');
res.redirect(this.href());
} catch (ex) {
res.message = ex;
app.log(ex);
}
}
res.data.action = this.href(req.action);
res.data.title = gettext('Edit Image');
res.data.body = this.renderSkinAsString('$Image#edit');
res.handlers.site.renderSkin('Site#page');
return;
}
/**
*
* @param {String} name
* @returns {Object}
*/
Image.prototype.getFormValue = function(name) {
var self = this;
var getOrigin = function(str) {
var origin = req.postParams.file_origin || self.origin;
if (origin && origin.contains('://')) {
return origin;
}
return null;
}
if (req.isPost()) {
if (name === 'file') {
return getOrigin();
}
return req.postParams[name];
}
switch (name) {
case 'file':
return getOrigin();
case 'tags':
return this.getTags();
}
return this[name] || req.queryParams[name];
}
/**
*
* @param {Object} data
*/
Image.prototype.update = function(data) {
if (data.uploadError) {
app.log(data.uploadError);
throw Error(gettext('File size is exceeding the upload limit.'));
}
var mime = data.file;
var origin = data.file_origin;
var isLayout = (this.parent_type === 'Layout' || path && !!path.layout);
if (!mime || mime.contentLength < 1) {
if (origin && origin !== this.origin) {
var http = new helma.Http();
http.setBinaryMode(true);
http.setHeader('Accept', 'image/*');
http.setHeader('Accept-Encoding', 'gzip');
http.setHeader('Cache-Control', 'no-cache');
http.setHeader('Pragma', 'no-cache');
http.setHeader('Referer', origin);
http.setHeader('User-Agent', req.data.http_browser);
var response = http.getUrl(origin);
mime = new Packages.helma.util.MimePart(
origin,
response.content,
response.type
);
if (!mime) {
throw Error(gettext('Could not fetch the file from the given URL.'));
}
} else if (this.isTransient()) {
throw Error(gettext('There was nothing to upload. Please be sure to choose a file.'));
}
}
if (mime.contentLength > 0) {
var extension = Image.getFileExtension(mime.contentType);
if (!extension) {
throw Error(gettext('This does not seem to be a valid JPG, PNG or GIF image.'));
}
var mimeName = mime.normalizeFilename(mime.name);
this.contentLength = mime.contentLength;
this.contentType = mime.contentType;
File.prototype.setOrigin.call(this, origin);
if (this.isTransient()) {
var name = data.name || mimeName.replace(/\.[^.]+$/, '');
this.name = this.parent.images.getAccessName(name);
}
var thumbnail;
var image = this.getHelmaImage(mime, isLayout ? null :
res.handlers.site.imageDimensionLimits);
this.width = image.width;
this.height = image.height;
// Create a thumbnail version if the image size exceeds constraints
if (this.width > Image.THUMBNAILWIDTH) {
thumbnail = this.getHelmaImage(mime, [Image.THUMBNAILWIDTH]);
this.thumbnailWidth = thumbnail.width;
this.thumbnailHeight = thumbnail.height;
} else if (this.isPersistent()) {
this.getThumbnailFile().remove();
// NOTE: delete operator won't work here due to getter/setter methods
this.deleteMetadata('thumbnailName', 'thumbnailWidth', 'thumbnailHeight');
}
// Make the image persistent before proceeding with writing files and setting tags (also see Helma bug #607)
if (this.isTransient()) {
this.persist();
}
var fileName = File.getName(this.name) + extension;
// Remove existing image files if the file name has changed
if (fileName !== this.fileName) {
this.removeFiles();
}
this.fileName = fileName;
if (thumbnail) {
this.thumbnailName = fileName.replace(/(\.[^.]+$)/, '-small$1');
}
this.writeFiles(image.data || mime, thumbnail && thumbnail.data);
this.contentLength = this.getFile().getLength();
}
// Layout images cannot be tagged
if (!isLayout) {
this.setTags(data.tags || data.tag_array);
}
this.description = data.description;
this.touch();
return;
}
Image.prototype.getMacroHandler = function (name) {
switch (name) {
case 'site':
return this.parent;
}
}
/**
*
*/
Image.prototype.tags_macro = function() {
return res.write(this.getFormValue('tags'));
}
/**
*
*/
Image.prototype.contentLength_macro = function() {
return res.write((this.contentLength / 1024).format('###,###') + ' KB');
}
/**
*
*/
Image.prototype.url_macro = function() {
return res.write(encodeURI(this.getUrl()));
}
/**
*
*/
Image.prototype.macro_macro = function(param) {
if (this.parent && this.parent.constructor === Layout) {
param.suffix = null;
return HopObject.prototype.macro_macro.call(this, param, 'layout.image');
}
return HopObject.prototype.macro_macro.call(this, param, 'image');
}
/**
*
* @param {Object} param
*/
Image.prototype.thumbnail_macro = function(param) {
if (!this.thumbnailName) {
return this.render_macro(param);
}
param.src = encodeURI(this.getUrl(this.getThumbnailFile().getName()));
if (!param.title) { param.title = encode(this.description || ''); }
param.alt = encode(param.alt || param.title);
param.loading = "lazy";
var width = param.width || this.thumbnailWidth;
var height = param.height || this.thumbnailHeight;
var style = [];
width && style.push('width:', width + 'px;');
height && style.push('height:', height + 'px;');
param.border && style.push('border:', param.border + 'px;');
param.style = style.join(String.SPACE);
delete param.width;
delete param.height;
delete param.border;
html.tag('img', param);
return;
}
/**
*
* @param {Object} param
*/
Image.prototype.render_macro = function(param) {
param.src = encodeURI(this.getUrl());
if (!param.title) { param.title = encode(this.description || ''); }
param.alt = encode(param.alt || param.title);
param.loading = "lazy";
var style = [];
param.width && style.push('width:', param.width + 'px;');
param.height && style.push('height:', param.height + 'px;');
param.border && style.push('border:', param.border + 'px;');
param.style = style.join(String.SPACE);
delete param.width;
delete param.height;
delete param.border;
html.tag('img', param);
return;
};
Image.prototype.description_macro = function() {
File.prototype.description_macro.apply(this, arguments);
};
/**
*
* @param {Object} name
* @returns {helma.File}
* @see Site#getStaticFile
*/
Image.prototype.getFile = function(name) {
name || (name = this.fileName);
if (this.parent_type === 'Layout') {
var layout = this.parent || res.handlers.layout;
return layout.getFile(name);
}
var site = this.parent || res.handlers.site;
return site.getStaticFile('images/' + name);
}
/**
*
* @param {Object} name
* @returns {String}
* @see Site#getStaticUrl
*/
Image.prototype.getUrl = function(name) {
name || (name = this.fileName);
if (this.parent_type === 'Layout') {
var layout = this.parent || res.handlers.layout;
var url = String.EMPTY;
try {
url = layout.site.getStaticUrl('layout/' + name);
} catch (ex) {
console.error(ex);
console.error(this.toSource());
}
return url;
}
var site = this.parent || res.handlers.site;
return site.getStaticUrl('images/' + name);
}
/**
* @returns {helma.File}
*/
Image.prototype.getThumbnailFile = function() {
return this.getFile(this.thumbnailName);
}
/**
* @returns {String}
*/
Image.prototype.getJSON = function() {
return {
name: this.name,
origin: this.origin,
description: this.description,
contentType: this.contentType,
contentLength: this.contentLength,
width: this.width,
height: this.height,
thumbnailName: this.thumbnailName,
thumbnailWidth: this.thumbnailWidth,
thumbnailHeight: this.thumbnailHeight,
created: this.created,
creator: this.creator ? this.creator.name : null,
modified: this.modified,
modifier: this.modifier ? this.modifier.name : null,
}.toSource();
}
/**
*
* @param {helma.util.MimePart} mime
* @param {Array} dimensionLimits [maxWidth, maxHeight]
* @throws {Error}
* @returns {Object}
*/
Image.prototype.getHelmaImage = function(mime, dimensionLimits) {
if (!dimensionLimits) dimensionLimits = [];
var maxWidth = dimensionLimits[0] || Infinity;
var maxHeight = dimensionLimits[1] || Infinity;
var result = {
data: null,
width: 0,
height: 0
};
try {
var image = new helma.Image(mime.inputStream);
var factorH = 1, factorV = 1;
if (maxWidth && image.width > maxWidth) {
factorH = maxWidth / image.width;
}
if (maxHeight && image.height > maxHeight) {
factorV = maxHeight / image.height;
}
if (factorH !== 1 || factorV !== 1) {
var width = Math.ceil(image.width *
(factorH < factorV ? factorH : factorV));
var height = Math.ceil(image.height *
(factorH < factorV ? factorH : factorV));
image.resize(width, height);
if (mime.contentType.endsWith('gif')) {
image.reduceColors(256);
}
result.data = image;
}
result.width = image.width;
result.height = image.height;
return result;
} catch (ex) {
app.log(ex);
throw Error(gettext('Could not resize the image.'));
}
}
/**
*
* @param {helma.Image|helma.util.MimePart} data
* @param {Object} thumbnail
* @throws {Error}
*/
Image.prototype.writeFiles = function(data, thumbnail) {
if (data) {
try {
// If data is a MimeObject (ie. has the writeToFile method)
// the image was not resized and thus, we directly write it to disk
var file = this.getFile();
if (data.saveAs) {
data.saveAs(file);
} else if (data.writeToFile) {
data.writeToFile(file.getParent(), file.getName());
}
if (thumbnail) {
thumbnail.saveAs(this.getThumbnailFile());
}
} catch (ex) {
app.log(ex);
throw Error(gettext('Could not save the image file on disk.'));
}
}
return;
}
/**
* @throws {Error}
*/
Image.prototype.removeFiles = function() {
try {
this.getFile().remove();
var thumbnail = this.getThumbnailFile();
if (thumbnail) {
thumbnail.remove();
}
} catch (ex) {
app.log(ex);
throw Error(gettext('Could not remove the image file from disk.'));
}
return;
}
/**
* @returns {String}
*/
Image.prototype.getConfirmText = function() {
return gettext('You are about to delete the image {0}.', this.name);
}