624 lines
16 KiB
JavaScript
624 lines
16 KiB
JavaScript
// The Antville Project
|
||
// http://code.google.com/p/antville
|
||
//
|
||
// Copyright 2001–2014 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);
|
||
}
|