/** * Copyright (C) 2010-2017 Structr GmbH * * This file is part of Structr <http://structr.org>. * * Structr is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * Structr is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Structr. If not, see <http://www.gnu.org/licenses/>. */ package org.structr.web.entity; import groovyjarjarantlr.StringUtils; import java.io.IOException; import java.util.LinkedList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.structr.common.PropertyView; import org.structr.common.SecurityContext; import org.structr.common.error.ErrorBuffer; import org.structr.common.error.FrameworkException; import org.structr.core.app.App; import org.structr.core.app.StructrApp; import org.structr.core.entity.AbstractNode; import org.structr.core.entity.AbstractRelationship; import org.structr.core.graph.ModificationQueue; import org.structr.core.property.BooleanProperty; import org.structr.core.property.ConstantBooleanProperty; import org.structr.core.property.IntProperty; import org.structr.core.property.Property; import org.structr.core.property.PropertyKey; import org.structr.core.property.PropertyMap; import org.structr.dynamic.File; import org.structr.schema.SchemaService; import org.structr.web.common.FileHelper; import org.structr.web.common.ImageHelper; import org.structr.web.common.ImageHelper.Thumbnail; import org.structr.web.entity.relation.Thumbnails; import org.structr.web.property.ImageDataProperty; import org.structr.web.property.ThumbnailProperty; //~--- classes ---------------------------------------------------------------- /** * An image whose binary data will be stored on disk. * * * */ public class Image extends org.structr.dynamic.File { // register this type as an overridden builtin type static { SchemaService.registerBuiltinTypeOverride("Image", Image.class.getName()); } private static final Logger logger = LoggerFactory.getLogger(Image.class.getName()); public static final Property<Integer> height = new IntProperty("height").cmis().indexed(); public static final Property<Integer> width = new IntProperty("width").cmis().indexed(); public static final Property<Integer> orientation = new IntProperty("orientation").cmis().indexed(); public static final Property<Image> tnSmall = new ThumbnailProperty("tnSmall").format("100, 100, false"); public static final Property<Image> tnMid = new ThumbnailProperty("tnMid").format("300, 300, false"); public static final Property<Boolean> isThumbnail = new BooleanProperty("isThumbnail").indexed().unvalidated().systemInternal(); public static final ImageDataProperty imageData = new ImageDataProperty("imageData"); public static final Property<Boolean> isImage = new ConstantBooleanProperty("isImage", true); public static final Property<Boolean> isCreatingThumb = new BooleanProperty("isCreatingThumb").systemInternal(); public static final org.structr.common.View uiView = new org.structr.common.View(Image.class, PropertyView.Ui, type, name, contentType, size, relativeFilePath, width, height, orientation, tnSmall, tnMid, isThumbnail, owner, parent, path, isImage); public static final org.structr.common.View publicView = new org.structr.common.View(Image.class, PropertyView.Public, type, name, width, height, orientation, tnSmall, tnMid, isThumbnail, owner, parent, path, isImage); @Override public Object setProperty(final PropertyKey key, final Object value) throws FrameworkException { // Copy visibility properties and owner to all thumbnails if (visibleToPublicUsers.equals(key) || visibleToAuthenticatedUsers.equals(key) || visibilityStartDate.equals(key) || visibilityEndDate.equals(key) || owner.equals(key)) { for (Image tn : getThumbnails()) { tn.setProperty(key, value); } } return super.setProperty(key, value); } @Override public void setProperties(final SecurityContext securityContext, final PropertyMap properties) throws FrameworkException { if ( !isThumbnail() ) { final PropertyMap propertiesCopiedToAllThumbnails = new PropertyMap(); for (final PropertyKey key : properties.keySet()) { if (visibleToPublicUsers.equals(key) || visibleToAuthenticatedUsers.equals(key) || visibilityStartDate.equals(key) || visibilityEndDate.equals(key) || owner.equals(key)) { propertiesCopiedToAllThumbnails.put(key, properties.get(key)); } } if ( !propertiesCopiedToAllThumbnails.isEmpty() ) { final List<Image> thumbnails = getThumbnails(); for (Image tn : thumbnails) { tn.setProperties(tn.getSecurityContext(), propertiesCopiedToAllThumbnails); } } } super.setProperties(securityContext, properties); } @Override public boolean onModification(final SecurityContext securityContext, final ErrorBuffer errorBuffer, final ModificationQueue modificationQueue) throws FrameworkException { if (super.onModification(securityContext, errorBuffer, modificationQueue)) { if ( !isThumbnail() ) { if (modificationQueue.isPropertyModified(this, name)) { final String newImageName = getName(); for (Image tn : getThumbnails()) { final String expectedThumbnailName = ImageHelper.getThumbnailName(newImageName, tn.getWidth(), tn.getHeight()); final String currentThumbnailName = tn.getName(); if ( !expectedThumbnailName.equals(currentThumbnailName) ) { logger.debug("Auto-renaming Thumbnail({}) from '{}' to '{}'", tn.getUuid(), currentThumbnailName, expectedThumbnailName); tn.setProperty(AbstractNode.name, expectedThumbnailName); } } } } return true; } return false; } //~--- get methods ---------------------------------------------------- public Integer getWidth() { return getProperty(Image.width); } public Integer getHeight() { return getProperty(Image.height); } public List<Image> getThumbnails() { final List<Image> thumbnails = new LinkedList<>(); for (final AbstractRelationship s : getThumbnailRelationships()) { thumbnails.add((Image) s.getTargetNode()); } return thumbnails; } /** * Get thumbnail relationships * * @return thumbnails */ public Iterable<Thumbnails> getThumbnailRelationships() { return getOutgoingRelationships(Thumbnails.class); } /** * Get (down-)scaled image of this image * * If no scaled image of the requested size exists or the image is newer than the scaled image, create a new one * * @param maxWidthString * @param maxHeightString * * @return scaled image */ public Image getScaledImage(final String maxWidthString, final String maxHeightString) { return getScaledImage(Integer.parseInt(maxWidthString), Integer.parseInt(maxHeightString), false); } public Image getScaledImage(final String maxWidthString, final String maxHeightString, final boolean cropToFit) { return getScaledImage(Integer.parseInt(maxWidthString), Integer.parseInt(maxHeightString), cropToFit); } public Image getScaledImage(final int maxWidth, final int maxHeight) { return getScaledImage(maxWidth, maxHeight, false); } /** * Get (down-)scaled image of this image * * If no scaled image of the requested size exists or the image is newer than the scaled image, create a new one. * * Default behaviour is to make the scaled image complete fit inside a rectangle of maxWidth x maxHeight. * * @param maxWidth * @param maxHeight * @param cropToFit if true, scale down until the shorter edge fits inside the rectangle, and then crop * * @return scaled image */ public Image getScaledImage(final int maxWidth, final int maxHeight, final boolean cropToFit) { final Iterable<Thumbnails> thumbnailRelationships = getThumbnailRelationships(); final List<Image> oldThumbnails = new LinkedList<>(); Image thumbnail = null; final Image originalImage = this; final Integer origWidth = originalImage.getWidth(); final Integer origHeight = originalImage.getHeight(); final Long currentChecksum = originalImage.getProperty(Image.checksum); final Long newChecksum; if (currentChecksum == null || currentChecksum == 0) { newChecksum = FileHelper.getChecksum(originalImage); } else { newChecksum = currentChecksum; } // Return self if SVG image final String _contentType = getProperty(Image.contentType); if (_contentType != null && (_contentType.startsWith("image/svg") || (_contentType.startsWith("image/") && _contentType.endsWith("icon")))) { return this; } if (origWidth != null && origHeight != null && thumbnailRelationships != null) { for (final Thumbnails r : thumbnailRelationships) { final Integer w = r.getProperty(Image.width); final Integer h = r.getProperty(Image.height); if (w != null && h != null) { // orginal image is equal or smaller than requested size if (((w == maxWidth) && (h <= maxHeight)) || ((w <= maxWidth) && (h == maxHeight)) || ((origWidth <= w) && (origHeight <= h))) { thumbnail = r.getTargetNode(); // Use thumbnail only if checksum of original image matches with stored checksum final Long storedChecksum = r.getProperty(Image.checksum); if (storedChecksum != null && storedChecksum.equals(newChecksum)) { return thumbnail; } else { oldThumbnails.add(thumbnail); } } } } } if (originalImage.getProperty(Image.isCreatingThumb).equals(Boolean.TRUE)) { logger.debug("Another thumbnail is being created - waiting...."); } else { try { // No thumbnail exists, or thumbnail was too old, so let's create a new one logger.debug("Creating thumbnail for {} (w={} h={} crop={})", new Object[] { getName(), maxWidth, maxHeight, cropToFit }); originalImage.unlockSystemPropertiesOnce(); originalImage.setProperty(Image.isCreatingThumb, Boolean.TRUE); final App app = StructrApp.getInstance(securityContext); originalImage.unlockSystemPropertiesOnce(); originalImage.setProperty(File.checksum, newChecksum); final Thumbnail thumbnailData = ImageHelper.createThumbnail(originalImage, maxWidth, maxHeight, cropToFit); if (thumbnailData != null) { final Integer tnWidth = thumbnailData.getWidth(); final Integer tnHeight = thumbnailData.getHeight(); byte[] data = null; try { data = thumbnailData.getBytes(); final String thumbnailName = ImageHelper.getThumbnailName(originalImage.getName(), tnWidth, tnHeight); // create thumbnail node thumbnail = ImageHelper.createImageNode(securityContext, data, "image/" + Thumbnail.defaultFormat, Image.class, thumbnailName, true); } catch (IOException ex) { logger.warn("Could not create thumbnail image for " + getUuid(), ex); } if (thumbnail != null && data != null) { // Create a thumbnail relationship final PropertyMap relProperties = new PropertyMap(); relProperties.put(Image.width, tnWidth); relProperties.put(Image.height, tnHeight); relProperties.put(Image.checksum, newChecksum); app.create(originalImage, thumbnail, Thumbnails.class, relProperties); final PropertyMap properties = new PropertyMap(); properties.put(Image.width, tnWidth); properties.put(Image.height, tnHeight); properties.put(AbstractNode.hidden, originalImage.getProperty(AbstractNode.hidden)); properties.put(AbstractNode.visibleToAuthenticatedUsers, originalImage.getProperty(AbstractNode.visibleToAuthenticatedUsers)); properties.put(AbstractNode.visibleToPublicUsers, originalImage.getProperty(AbstractNode.visibleToPublicUsers)); properties.put(File.size, Long.valueOf(data.length)); properties.put(AbstractNode.owner, originalImage.getProperty(AbstractNode.owner)); properties.put(File.parent, originalImage.getProperty(File.parent)); thumbnail.unlockSystemPropertiesOnce(); thumbnail.setProperties(securityContext, properties); // Delete outdated thumbnails for (final Image tn : oldThumbnails) { app.delete(tn); } } } else { logger.debug("Could not create thumbnail for image {} ({})", getName(), getUuid()); } originalImage.unlockSystemPropertiesOnce(); originalImage.removeProperty(Image.isCreatingThumb); } catch (FrameworkException fex) { logger.warn("Unable to create thumbnail for " + getUuid(), fex); } } return thumbnail; } /** * Return true if this image is a thumbnail image. * * This is determined by having at least one incoming THUMBNAIL relationship * * @return true if is thumbnail */ public boolean isThumbnail() { return getProperty(Image.isThumbnail) || getIncomingRelationship(Thumbnails.class) != null; } /** * @return the name of the original image */ public String getOriginalImageName() { final Integer tnWidth = getWidth(); final Integer tnHeight = getHeight(); return StringUtils.stripBack(getName(), "_thumb_" + tnWidth + "x" + tnHeight); } }