From 71e8e4ba8558ba8b86aefe66dd7f8d255a91008e Mon Sep 17 00:00:00 2001 From: Gethin James Date: Mon, 5 Dec 2011 14:46:57 +0000 Subject: [PATCH] FIXED : ALF-10578: iPad uploaded files appear upside down in share preview Defaults to auto-orient images based on EXIF info git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@32539 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../transform/magick/ImageCropOptions.java | 11 ++ .../ImageMagickContentTransformerWorker.java | 4 + .../transform/magick/ImageResizeOptions.java | 12 ++ .../magick/ImageTransformationOptions.java | 34 +++-- .../RenditionServiceIntegrationTest.java | 135 +++++++++++++++--- .../executer/ImageRenderingEngine.java | 18 +++ .../ThumbnailRenditionConvertor.java | 1 + .../images/rotated_gray21.512.jpg | Bin 0 -> 10259 bytes 8 files changed, 185 insertions(+), 30 deletions(-) create mode 100644 source/test-resources/images/rotated_gray21.512.jpg 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 0000000000000000000000000000000000000000..80c51d58f139b05bb7756e81bbff22ac7469ec4f GIT binary patch literal 10259 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 H2EY4H*;{5y literal 0 HcmV?d00001