diff --git a/source/java/org/alfresco/repo/content/transform/magick/ImageCropOptions.java b/source/java/org/alfresco/repo/content/transform/magick/ImageCropOptions.java index 38b4c467a2..590696e502 100644 --- a/source/java/org/alfresco/repo/content/transform/magick/ImageCropOptions.java +++ b/source/java/org/alfresco/repo/content/transform/magick/ImageCropOptions.java @@ -166,4 +166,15 @@ public class ImageCropOptions { return this.gravity; } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append("ImageCropOptions [height=").append(this.height).append(", width=").append(this.width) + .append(", xOffset=").append(this.xOffset).append(", yOffset=").append(this.yOffset) + .append(", isPercentageCrop=").append(this.isPercentageCrop).append(", gravity=") + .append(this.gravity).append("]"); + return builder.toString(); + } } diff --git a/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformerWorker.java b/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformerWorker.java index 0e58894249..c06f444544 100644 --- a/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformerWorker.java +++ b/source/java/org/alfresco/repo/content/transform/magick/ImageMagickContentTransformerWorker.java @@ -154,6 +154,10 @@ public class ImageMagickContentTransformerWorker extends AbstractImageMagickCont ImageCropOptions cropOptions = imageOptions.getCropOptions(); ImageResizeOptions resizeOptions = imageOptions.getResizeOptions(); String commandOptions = imageOptions.getCommandOptions(); + if (imageOptions.isAutoOrient()) + { + commandOptions = commandOptions + " -auto-orient"; + } if (cropOptions != null) { commandOptions = commandOptions + " " + getImageCropCommandOptions(cropOptions); diff --git a/source/java/org/alfresco/repo/content/transform/magick/ImageResizeOptions.java b/source/java/org/alfresco/repo/content/transform/magick/ImageResizeOptions.java index 4f94c97ab5..5c1783d149 100644 --- a/source/java/org/alfresco/repo/content/transform/magick/ImageResizeOptions.java +++ b/source/java/org/alfresco/repo/content/transform/magick/ImageResizeOptions.java @@ -114,4 +114,16 @@ public class ImageResizeOptions { return allowEnlargement; } + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append("ImageResizeOptions [width=").append(this.width).append(", height=").append(this.height) + .append(", maintainAspectRatio=").append(this.maintainAspectRatio).append(", percentResize=") + .append(this.percentResize).append(", resizeToThumbnail=").append(this.resizeToThumbnail) + .append(", allowEnlargement=").append(this.allowEnlargement).append("]"); + return builder.toString(); + } + } diff --git a/source/java/org/alfresco/repo/content/transform/magick/ImageTransformationOptions.java b/source/java/org/alfresco/repo/content/transform/magick/ImageTransformationOptions.java index 539affc26e..f05044ada4 100644 --- a/source/java/org/alfresco/repo/content/transform/magick/ImageTransformationOptions.java +++ b/source/java/org/alfresco/repo/content/transform/magick/ImageTransformationOptions.java @@ -33,7 +33,8 @@ public class ImageTransformationOptions extends TransformationOptions public static final String OPT_COMMAND_OPTIONS = "commandOptions"; public static final String OPT_IMAGE_RESIZE_OPTIONS = "imageResizeOptions"; public static final String OPT_IMAGE_CROP_OPTIONS = "imageCropOptions"; - + public static final String OPT_IMAGE_AUTO_ORIENTATION = "imageAutoOrient"; + /** Command string options, provided for backward compatibility */ private String commandOptions = ""; @@ -43,6 +44,7 @@ public class ImageTransformationOptions extends TransformationOptions /** Image crop options */ private ImageCropOptions cropOptions; + private boolean autoOrient = true; /** * Set the command string options * @@ -86,13 +88,11 @@ public class ImageTransformationOptions extends TransformationOptions @Override public String toString() { - StringBuilder msg = new StringBuilder(100); - msg.append(this.getClass().getSimpleName()) - .append("[ commandOptions=").append(commandOptions) - .append(", resizeOptions=").append(resizeOptions) - .append("]"); - - return msg.toString(); + StringBuilder builder = new StringBuilder(); + builder.append("ImageTransformationOptions [commandOptions=").append(this.commandOptions) + .append(", resizeOptions=").append(this.resizeOptions).append(", cropOptions=") + .append(this.cropOptions).append(", autoOrient=").append(this.autoOrient).append("]"); + return builder.toString(); } /** @@ -106,6 +106,7 @@ public class ImageTransformationOptions extends TransformationOptions props.put(OPT_COMMAND_OPTIONS, commandOptions); props.put(OPT_IMAGE_RESIZE_OPTIONS, resizeOptions); props.put(OPT_IMAGE_CROP_OPTIONS, cropOptions); + props.put(OPT_IMAGE_AUTO_ORIENTATION, autoOrient); return props; } @@ -125,4 +126,21 @@ public class ImageTransformationOptions extends TransformationOptions { return this.cropOptions; } + + /** + * @return Will the image be automatically oriented(rotated) based on the EXIF "Orientation" data. + * Defaults to TRUE + */ + public boolean isAutoOrient() + { + return this.autoOrient; + } + + /** + * @param autoOrient automatically orient (rotate) based on the EXIF "Orientation" data + */ + public void setAutoOrient(boolean autoOrient) + { + this.autoOrient = autoOrient; + } } diff --git a/source/java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java b/source/java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java index b43a2ac8eb..2715b170e7 100644 --- a/source/java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java +++ b/source/java/org/alfresco/repo/rendition/RenditionServiceIntegrationTest.java @@ -21,6 +21,7 @@ package org.alfresco.repo.rendition; import java.awt.image.BufferedImage; import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.URL; @@ -81,6 +82,8 @@ import org.springframework.context.ConfigurableApplicationContext; @SuppressWarnings("deprecation") public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest { + private static final String WHITE = "ffffff"; + private static final String BLACK = "000000"; private final static QName REFORMAT_RENDER_DEFN_NAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, ReformatRenderingEngine.NAME + System.currentTimeMillis()); private final static QName RESCALE_RENDER_DEFN_NAME = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, @@ -146,19 +149,12 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest // Create a test image - this.nodeWithImageContent = createContentNode(companyHome, "testImageNode"); - // Stream some well-known image content into the node. + this.nodeWithImageContent = createContentNode(companyHome, "testImageNode"); + // Stream some well-known image content into the node. URL url = RenditionServiceIntegrationTest.class.getClassLoader().getResource("images/gray21.512.png"); assertNotNull("url of test image was null", url); File imageFile = new File(url.getFile()); - assertTrue(imageFile.exists()); - - nodeService.setProperty(nodeWithImageContent, ContentModel.PROP_CONTENT, new ContentData(null, - MimetypeMap.MIMETYPE_IMAGE_PNG, 0L, null)); - writer = contentService.getWriter(nodeWithImageContent, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_IMAGE_PNG); - writer.setEncoding("UTF-8"); - writer.putContent(imageFile); + setImageContentOnNode(nodeWithImageContent, MimetypeMap.MIMETYPE_IMAGE_PNG, imageFile); // Create a test template node. this.nodeWithFreeMarkerContent = createFreeMarkerNode(companyHome); @@ -168,6 +164,17 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest { return createNode(companyHome, name, ContentModel.TYPE_CONTENT); } + + private void setImageContentOnNode(NodeRef nodeWithImage, String mimetypeImage, File imageFile) + { + assertTrue(imageFile.exists()); + nodeService.setProperty(nodeWithImage, ContentModel.PROP_CONTENT, new ContentData(null, + mimetypeImage, 0L, null)); + ContentWriter writer = contentService.getWriter(nodeWithImage, ContentModel.PROP_CONTENT, true); + writer.setMimetype(mimetypeImage); + writer.setEncoding("UTF-8"); + writer.putContent(imageFile); + } private NodeRef createFreeMarkerNode(NodeRef companyHome) { @@ -649,7 +656,7 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest parameterValues.put(ImageRenderingEngine.PARAM_CROP_HEIGHT, imageNewYSize); ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); - final NodeRef newRenditionNode = performImageRendition(parameterValues); + final NodeRef newRenditionNode = performImageRendition(parameterValues,nodeWithImageContent); // Assert that the rendition is of the correct size and has reasonable // content. @@ -704,7 +711,7 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest parameterValues.put(ImageRenderingEngine.PARAM_CROP_HEIGHT, 25); // 128 pixels parameterValues.put(ImageRenderingEngine.PARAM_IS_PERCENT_CROP, true); - final NodeRef secondRenditionNode = performImageRendition(parameterValues); + final NodeRef secondRenditionNode = performImageRendition(parameterValues,nodeWithImageContent); // Assert that the rendition is of the correct size and has reasonable // content. @@ -772,7 +779,7 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, imageNewXSize); parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, imageNewYSize); - final NodeRef newRenditionNode = performImageRendition(parameterValues); + final NodeRef newRenditionNode = performImageRendition(parameterValues, nodeWithImageContent); // Assert that the rendition is of the correct size and has reasonable // content. @@ -802,11 +809,11 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest // The upper left pixel of the image should be pure black. int rgbAtTopLeft = img.getRGB(1, 1); - assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith("000000")); + assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith(BLACK)); // The lower right pixel of the image should be pure white int rgbAtBottomRight = img.getRGB(img.getWidth() - 1, img.getHeight() - 1); - assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith("ffffff")); + assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith(WHITE)); return null; } @@ -818,7 +825,7 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, 200); parameterValues.put(ImageRenderingEngine.PARAM_IS_PERCENT_RESIZE, true); - final NodeRef secondRenditionNode = performImageRendition(parameterValues); + final NodeRef secondRenditionNode = performImageRendition(parameterValues, nodeWithImageContent); // Assert that the rendition is of the correct size and has reasonable // content. @@ -848,11 +855,11 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest // The upper left pixel of the image should be pure black. int rgbAtTopLeft = img.getRGB(1, 1); - assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith("000000")); + assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith(BLACK)); // The lower right pixel of the image should be pure white int rgbAtBottomRight = img.getRGB(img.getWidth() - 1, img.getHeight() - 1); - assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith("ffffff")); + assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith(WHITE)); return null; } @@ -905,11 +912,11 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest // The upper left pixel of the image should be pure black. int rgbAtTopLeft = img.getRGB(1, 1); - assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith("000000")); + assertTrue("Incorrect image content.", Integer.toHexString(rgbAtTopLeft).endsWith(BLACK)); // The lower right pixel of the image should be pure white int rgbAtBottomRight = img.getRGB(img.getWidth() - 1, img.getHeight() - 1); - assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith("ffffff")); + assertTrue("Incorrect image content.", Integer.toHexString(rgbAtBottomRight).endsWith(WHITE)); return null; } @@ -1175,7 +1182,7 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest }); } - private NodeRef performImageRendition(final Map parameterValues) + protected NodeRef performImageRendition(final Map parameterValues, final NodeRef imageToRender) { final NodeRef newRenditionNode = transactionHelper .doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() @@ -1197,7 +1204,7 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest action.setParameterValue(s, parameterValues.get(s)); } - ChildAssociationRef renditionAssoc = renditionService.render(nodeWithImageContent, action); + ChildAssociationRef renditionAssoc = renditionService.render(imageToRender, action); validateRenditionAssociation(renditionAssoc, RESCALE_RENDER_DEFN_NAME); @@ -2218,6 +2225,90 @@ public class RenditionServiceIntegrationTest extends BaseAlfrescoSpringTest this.scriptService.executeScript(location, model); } + /** + * This test method takes an image with Exif Orientation information and checks if it is automatially rotated. + */ + public void testAutoRotateImage() throws Exception + { + NodeRef companyHome = repositoryHelper.getCompanyHome(); + + //Check image is there + String imageSource = "images/rotated_gray21.512.jpg"; + File imageFile = retrieveValidImageFile(imageSource); + + //Create node and save contents + final NodeRef newNodeForRotate = createNode(companyHome, "rotateImageNode", ContentModel.TYPE_CONTENT); + setImageContentOnNode(newNodeForRotate, MimetypeMap.MIMETYPE_IMAGE_JPEG, imageFile); + + //Test auto rotates + final Map parameterValues = new HashMap(); + resizeImageAndCheckOrientation(newNodeForRotate, parameterValues, BLACK, WHITE); + + //Test doesn't auto rotate + parameterValues.clear(); + parameterValues.put(ImageRenderingEngine.PARAM_AUTO_ORIENTATION, false); + resizeImageAndCheckOrientation(newNodeForRotate, parameterValues, WHITE, BLACK); + + //Clean up + nodeService.deleteNode(newNodeForRotate); + } + + private void resizeImageAndCheckOrientation(final NodeRef nodeToResize, final Map parameterValues, final String topLeft, final String bottomRight) + { + //Resize to 100 by 100 + final Integer imageNewXSize = new Integer(100); + final Integer imageNewYSize = new Integer(100); + parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_WIDTH, imageNewXSize); + parameterValues.put(ImageRenderingEngine.PARAM_RESIZE_HEIGHT, imageNewYSize); + + final NodeRef newRenditionNode = performImageRendition(parameterValues, nodeToResize); + + // Assert that the rendition is of the correct size and has reasonable content. + transactionHelper.doInTransaction(new RetryingTransactionHelper.RetryingTransactionCallback() + { + public Void execute() throws Throwable + { + // The rescaled image rendition is a child of the original test + // node. + List children = nodeService.getChildAssocs(nodeToResize, + new RegexQNamePattern(getLongNameWithEscapedBraces(RenditionModel.ASSOC_RENDITION)), + new RegexQNamePattern(getLongNameWithEscapedBraces(RESCALE_RENDER_DEFN_NAME))); + + // There should only be one child of the image node: the + // rendition we've just created. + assertEquals("Unexpected number of children", 1, children.size()); + + NodeRef newImageRendition = children.get(0).getChildRef(); + assertEquals(newRenditionNode, newImageRendition); + + ContentReader reader = contentService.getReader(newImageRendition, ContentModel.PROP_CONTENT); + assertNotNull("Reader to rendered image was null", reader); + BufferedImage srcImg = ImageIO.read(reader.getContentInputStream()); + checkTopLeftBottomRight(srcImg, topLeft, bottomRight); + + return null; + } + }); + } + + private static File retrieveValidImageFile(String imageSource) throws IOException + { + URL url = RenditionServiceIntegrationTest.class.getClassLoader().getResource(imageSource); + File imageFile = new File(url.getFile()); + BufferedImage img = ImageIO.read(url); + assertNotNull("image was null", img); + checkTopLeftBottomRight(img, WHITE, BLACK); + return imageFile; + } + + private static void checkTopLeftBottomRight(BufferedImage img, String topLeft, String bottomRight) + { + int rgbAtTopLeft = img.getRGB(1, 1); + assertTrue("upper left should be "+topLeft, Integer.toHexString(rgbAtTopLeft).endsWith(topLeft)); + int rgbAtBottomRight = img.getRGB(img.getWidth() - 1, img.getHeight() - 1); + assertTrue("lower right should be "+bottomRight, Integer.toHexString(rgbAtBottomRight).endsWith(bottomRight)); + } + /** * A dummy rendering engine used in testing */ diff --git a/source/java/org/alfresco/repo/rendition/executer/ImageRenderingEngine.java b/source/java/org/alfresco/repo/rendition/executer/ImageRenderingEngine.java index 15de051f9d..b065f09dcc 100644 --- a/source/java/org/alfresco/repo/rendition/executer/ImageRenderingEngine.java +++ b/source/java/org/alfresco/repo/rendition/executer/ImageRenderingEngine.java @@ -229,6 +229,17 @@ public class ImageRenderingEngine extends AbstractTransformationRenderingEngine */ public static final String PARAM_COMMAND_OPTIONS = "commandOptions"; + /** + * This optional {@link Boolean} flag parameter specifies if the engine should + * automatically rotate and image based on the EXIF orientation flag. If + * this parameter is set to true then the engine reads + * and resets the EXIF image profile setting 'Orientation' and then performs + * the appropriate 90 degree rotation on the image to orient the image, + * for correct viewing. + * This parameter defaults to true. + */ + public static final String PARAM_AUTO_ORIENTATION = "autoOrientation"; + /* * @seeorg.alfresco.repo.rendition.executer.ReformatRenderingEngine# * getTransformOptions @@ -242,9 +253,12 @@ public class ImageRenderingEngine extends AbstractTransformationRenderingEngine ImageResizeOptions imageResizeOptions = getImageResizeOptions(context); ImageCropOptions cropOptions = getImageCropOptions(context); + boolean autoOrient = context.getParamWithDefault(PARAM_AUTO_ORIENTATION, true); + ImageTransformationOptions imageTransformationOptions = new ImageTransformationOptions(); imageTransformationOptions.setResizeOptions(imageResizeOptions); imageTransformationOptions.setCropOptions(cropOptions); + imageTransformationOptions.setAutoOrient(autoOrient); if (commandOptions != null) { imageTransformationOptions.setCommandOptions(commandOptions); @@ -359,6 +373,10 @@ public class ImageRenderingEngine extends AbstractTransformationRenderingEngine protected Collection getParameterDefinitions() { Collection paramList = super.getParameterDefinitions(); + + //Orientation + paramList.add(new ParameterDefinitionImpl(PARAM_AUTO_ORIENTATION, DataTypeDefinition.BOOLEAN, false, + getParamDisplayLabel(PARAM_AUTO_ORIENTATION))); //Resize Params paramList.add(new ParameterDefinitionImpl(PARAM_RESIZE_WIDTH, DataTypeDefinition.INT, false, diff --git a/source/java/org/alfresco/repo/thumbnail/ThumbnailRenditionConvertor.java b/source/java/org/alfresco/repo/thumbnail/ThumbnailRenditionConvertor.java index e55e07d593..9aa85dea5a 100644 --- a/source/java/org/alfresco/repo/thumbnail/ThumbnailRenditionConvertor.java +++ b/source/java/org/alfresco/repo/thumbnail/ThumbnailRenditionConvertor.java @@ -153,6 +153,7 @@ public class ThumbnailRenditionConvertor { ImageTransformationOptions imTransformationOptions = (ImageTransformationOptions)transformationOptions; putParameterIfNotNull(ImageRenderingEngine.PARAM_COMMAND_OPTIONS, imTransformationOptions.getCommandOptions(), parameters); + putParameterIfNotNull(ImageRenderingEngine.PARAM_AUTO_ORIENTATION, imTransformationOptions.isAutoOrient(), parameters); ImageResizeOptions imgResizeOptions = imTransformationOptions.getResizeOptions(); if (imgResizeOptions != null) diff --git a/source/test-resources/images/rotated_gray21.512.jpg b/source/test-resources/images/rotated_gray21.512.jpg new file mode 100644 index 0000000000..80c51d58f1 Binary files /dev/null and b/source/test-resources/images/rotated_gray21.512.jpg differ