diff --git a/engines/aio/src/test/java/org/alfresco/transform/aio/AIOTikaTest.java b/engines/aio/src/test/java/org/alfresco/transform/aio/AIOTikaTest.java index e4582a45..a1007538 100644 --- a/engines/aio/src/test/java/org/alfresco/transform/aio/AIOTikaTest.java +++ b/engines/aio/src/test/java/org/alfresco/transform/aio/AIOTikaTest.java @@ -64,6 +64,7 @@ public class AIOTikaTest extends TikaTest "page", "pageLimit", "pdfFormat", + "pdfOrientation", "resizeHeight", "resizePercentage", "resizeWidth", diff --git a/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformer.java b/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformer.java index e8c611e4..d6167cfb 100644 --- a/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformer.java +++ b/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformer.java @@ -28,6 +28,7 @@ package org.alfresco.transform.misc.transformers; import static org.alfresco.transform.common.RequestParamMap.END_PAGE; import static org.alfresco.transform.common.RequestParamMap.PDF_FORMAT; +import static org.alfresco.transform.common.RequestParamMap.PDF_ORIENTATION; import static org.alfresco.transform.common.RequestParamMap.START_PAGE; import javax.imageio.ImageIO; @@ -58,10 +59,11 @@ import org.springframework.stereotype.Component; * Converts image files into PDF files. Transformer uses PDF Box to perform conversions. * During conversion image might be scaled down (keeping proportions) to match width or height of the PDF document. * If the image is smaller than PDF page size, the image will be placed in the top left-hand side of the PDF document page. - * Transformer takes 3 optional transform options: - * - startPage - page number of image (for multipage images) from which transformer should start conversion. Default: first page of the image. - * - endPage - page number of image (for multipage images) up to which transformation should be performed. Default: last page of the image. - * - pdfFormat - output PDF file format. Available formats: A0, A1, A2, A3, A4, A5, A6, LETTER, LEGAL. Default: A4. + * Transformer accepts bellow optional transform parameters: + * - startPage - page number of image (for multi-page images) from which transformer should start conversion. Default: first page of the image. + * - endPage - page number of image (for multi-page images) up to which transformation should be performed. Default: last page of the image. + * - pdfFormat - output PDF file format. Available formats: DEFAULT, A0, A1, A2, A3, A4, A5, A6, LETTER, LEGAL. Default: original image size. + * - pdfOrientation - output PDF file orientation. Available options: DEFAULT, PORTRAIT, LANDSCAPE. Default: original image orientation. */ @Component public class ImageToPdfTransformer implements CustomTransformerFileAdaptor @@ -73,8 +75,8 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor private static final String START_PAGE_GREATER_THAN_END_PAGE_ERROR_MESSAGE = "Start page number cannot be greater than end page."; private static final String INVALID_OPTION_ERROR_MESSAGE = "Parameter '%s' is invalid: \"%s\" - it must be an integer."; private static final String INVALID_IMAGE_ERROR_MESSAGE = "Image file (%s) format (%s) not supported by ImageIO."; - private static final String DEFAULT_PDF_FORMAT_STRING = "A4"; - private static final PDRectangle DEFAULT_PDF_FORMAT = PDRectangle.A4; + private static final String DEFAULT_PDF_FORMAT_STRING = "DEFAULT"; + private static final String DEFAULT_PDF_ORIENTATION_STRING = "DEFAULT"; @Override public String getTransformerName() @@ -94,6 +96,7 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor final Integer startPage = parseOptionIfPresent(transformOptions, START_PAGE, Integer.class).orElse(null); final Integer endPage = parseOptionIfPresent(transformOptions, END_PAGE, Integer.class).orElse(null); final String pdfFormat = parseOptionIfPresent(transformOptions, PDF_FORMAT, String.class).orElse(DEFAULT_PDF_FORMAT_STRING); + final String pdfOrientation = parseOptionIfPresent(transformOptions, PDF_ORIENTATION, String.class).orElse(DEFAULT_PDF_ORIENTATION_STRING); verifyOptions(startPage, endPage); final ImageReader imageReader = findImageReader(imageInputStream, imageFile.getName(), sourceMimetype); @@ -108,7 +111,7 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor break; } - scaleAndDrawImage(pdfDocument, imageReader.read(i), pdfFormat); + scaleAndDrawImage(pdfDocument, imageReader.read(i), pdfFormat, pdfOrientation); } pdfDocument.save(pdfFile); @@ -128,11 +131,12 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor return imageReader; } - private void scaleAndDrawImage(final PDDocument pdfDocument, final BufferedImage bufferedImage, final String pdfFormat) throws IOException + private void scaleAndDrawImage(final PDDocument pdfDocument, final BufferedImage bufferedImage, final String pdfFormat, final String pdfOrientation) + throws IOException { - final PDPage pdfPage = new PDPage(resolvePdfFormat(pdfFormat)); - pdfDocument.addPage(pdfPage); final PDImageXObject image = LosslessFactory.createFromImage(pdfDocument, bufferedImage); + final PDPage pdfPage = new PDPage(resolvePdfFormat(pdfFormat, pdfOrientation, image.getWidth(), image.getHeight())); + pdfDocument.addPage(pdfPage); try (PDPageContentStream pdfPageContent = new PDPageContentStream(pdfDocument, pdfPage)) { final PDRectangle pageSize = pdfPage.getMediaBox(); @@ -146,31 +150,69 @@ public class ImageToPdfTransformer implements CustomTransformerFileAdaptor } } - private PDRectangle resolvePdfFormat(final String pdfFormat) + private PDRectangle resolvePdfFormat(final String pdfFormat, final String pdfOrientation, final int defaultWidth, final int defaultHeight) { - switch (pdfFormat.toUpperCase()) { + PDRectangle pdRectangle; + switch (pdfFormat.toUpperCase()) + { + case "DEFAULT": + pdRectangle = new PDRectangle(defaultWidth, defaultHeight); + break; case "A4": - return DEFAULT_PDF_FORMAT; + pdRectangle = PDRectangle.A4; + break; case "LETTER": - return PDRectangle.LETTER; + pdRectangle = PDRectangle.LETTER; + break; case "A0": - return PDRectangle.A0; + pdRectangle = PDRectangle.A0; + break; case "A1": - return PDRectangle.A1; + pdRectangle = PDRectangle.A1; + break; case "A2": - return PDRectangle.A2; + pdRectangle = PDRectangle.A2; + break; case "A3": - return PDRectangle.A3; + pdRectangle = PDRectangle.A3; + break; case "A5": - return PDRectangle.A5; + pdRectangle = PDRectangle.A5; + break; case "A6": - return PDRectangle.A6; + pdRectangle = PDRectangle.A6; + break; case "LEGAL": - return PDRectangle.LEGAL; + pdRectangle = PDRectangle.LEGAL; + break; default: - log.info("PDF format: '{}' not supported. Using default: '{}'", pdfFormat, DEFAULT_PDF_FORMAT_STRING); - return DEFAULT_PDF_FORMAT; + log.warn("PDF format: '{}' not supported. Maintaining the default one.", pdfFormat); + pdRectangle = new PDRectangle(defaultWidth, defaultHeight); + break; } + + switch (pdfOrientation.toUpperCase()) + { + case "DEFAULT": + break; + case "PORTRAIT": + if (pdRectangle.getWidth() > pdRectangle.getHeight()) + { + pdRectangle = new PDRectangle(pdRectangle.getHeight(), pdRectangle.getWidth()); + } + break; + case "LANDSCAPE": + if (pdRectangle.getHeight() > pdRectangle.getWidth()) + { + pdRectangle = new PDRectangle(pdRectangle.getHeight(), pdRectangle.getWidth()); + } + break; + default: + log.warn("PDF orientation: '{}' not supported. Maintaining the default one.", pdfOrientation); + break; + } + + return pdRectangle; } private static Optional parseOptionIfPresent(final Map transformOptions, final String parameter, final Class targetType) diff --git a/engines/misc/src/main/resources/misc_engine_config.json b/engines/misc/src/main/resources/misc_engine_config.json index 62c466c4..e32b1f90 100644 --- a/engines/misc/src/main/resources/misc_engine_config.json +++ b/engines/misc/src/main/resources/misc_engine_config.json @@ -12,7 +12,8 @@ "imageToPdfOptions": [ {"value": {"name": "startPage"}}, {"value": {"name": "endPage"}}, - {"value": {"name": "pdfFormat"}} + {"value": {"name": "pdfFormat"}}, + {"value": {"name": "pdfOrientation"}} ] }, "transformers": [ diff --git a/engines/misc/src/test/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformerTest.java b/engines/misc/src/test/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformerTest.java index cd519e49..64c81ed3 100644 --- a/engines/misc/src/test/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformerTest.java +++ b/engines/misc/src/test/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformerTest.java @@ -33,18 +33,23 @@ import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_TIFF; import static org.alfresco.transform.common.Mimetype.MIMETYPE_PDF; import static org.alfresco.transform.common.RequestParamMap.END_PAGE; import static org.alfresco.transform.common.RequestParamMap.PDF_FORMAT; +import static org.alfresco.transform.common.RequestParamMap.PDF_ORIENTATION; import static org.alfresco.transform.common.RequestParamMap.START_PAGE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.BDDMockito.then; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.Function; import java.util.stream.Stream; @@ -64,6 +69,8 @@ import org.mockito.MockitoAnnotations; class ImageToPdfTransformerTest { private static final File sourceFile = loadFile("sample.gif"); + private static final int sourceFileWidth; + private static final int sourceFileHeight; @Mock private TransformManager transformManager; @@ -108,16 +115,17 @@ class ImageToPdfTransformerTest TransformOptions.of(null, 0), // expected 1 page in target file TransformOptions.of(null, 1), // expected 2 pages in target file TransformOptions.of(0, null), // expected all pages in target file - TransformOptions.of(1, null), // expected 1 page in target file + TransformOptions.of(1, null), // expected all except first page in target file TransformOptions.none() // expected all pages in target file ); } static Stream transformSourcesAndOptions() { + ImageFile tiffImage = ImageFile.of("sample.tiff", MIMETYPE_IMAGE_TIFF, 6); return Stream.of( ArgumentsCartesianProduct.of(imageFiles(), defaultTransformOptions()), - ArgumentsCartesianProduct.of(ImageFile.of("sample.tiff", MIMETYPE_IMAGE_TIFF, 6), tiffTransformOptions()) + ArgumentsCartesianProduct.of(tiffImage, tiffTransformOptions()) ).flatMap(Function.identity()); } @@ -186,26 +194,65 @@ class ImageToPdfTransformerTest transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions, sourceFile, targetFile, transformManager)); } - static Stream validPdfFormats() + /** Option and expected dimensions. */ + static Stream validPdfFormats() { - return Stream.of("A0", "a0", "A1", "A2", "A3", "A4", "A5", "A6", "a6", "LETTER", "letter", "LEGAL", "legal"); + return Stream.of( + Arguments.of("DEFAULT", new PDRectangle(sourceFileWidth, sourceFileHeight)), + Arguments.of("default", new PDRectangle(sourceFileWidth, sourceFileHeight)), + Arguments.of("A0", PDRectangle.A0), + Arguments.of("a0", PDRectangle.A0), + Arguments.of("A1", PDRectangle.A1), + Arguments.of("A2", PDRectangle.A2), + Arguments.of("A3", PDRectangle.A3), + Arguments.of("A4", PDRectangle.A4), + Arguments.of("A5", PDRectangle.A5), + Arguments.of("A6", PDRectangle.A6), + Arguments.of("A6", PDRectangle.A6), + Arguments.of("LETTER", PDRectangle.LETTER), + Arguments.of("letter", PDRectangle.LETTER), + Arguments.of("LEGAL", PDRectangle.LEGAL), + Arguments.of("legal", PDRectangle.LEGAL) + ); + } + + /** Option and expected orientation. */ + static Stream validPdfOrientations() + { + return Stream.of( + Arguments.of("DEFAULT", unchangedRectangle()), + Arguments.of("default", unchangedRectangle()), + Arguments.of("PORTRAIT", rectangleRotatedIf((width, height) -> width > height)), + Arguments.of("portrait", rectangleRotatedIf((width, height) -> width > height)), + Arguments.of("LANDSCAPE", rectangleRotatedIf((width, height) -> height > width)), + Arguments.of("landscape", rectangleRotatedIf((width, height) -> height > width)) + ); + } + + static Stream validPdfFormatsAndOrientations() + { + return ArgumentsCartesianProduct.ofArguments( + validPdfFormats(), + validPdfOrientations() + ); } @ParameterizedTest - @MethodSource("validPdfFormats") - void testTransformImageToPDF_withVariousPdfFormats(String pdfFormat) throws Exception + @MethodSource("validPdfFormatsAndOrientations") + void testTransformImageToPDF_withVariousPdfFormatsAndOrientations(String pdfFormat, PDRectangle expectedPdfFormat, + String pdfOrientation, BiFunction expectedPdfFormatRotator) throws Exception { - TransformOptions transformOptions = TransformOptions.of(pdfFormat); + TransformOptions transformOptions = TransformOptions.of(pdfFormat, pdfOrientation); // when transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager); try (PDDocument actualPdfDocument = PDDocument.load(targetFile)) { - PDRectangle expectedPdfFormat = resolveExpectedPdfFormat(pdfFormat); + PDRectangle finalExpectedPdfFormat = expectedPdfFormatRotator.apply(expectedPdfFormat.getWidth(), expectedPdfFormat.getHeight()); assertNotNull(actualPdfDocument); - assertEquals(expectedPdfFormat.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); - assertEquals(expectedPdfFormat.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); + assertEquals(finalExpectedPdfFormat.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); + assertEquals(finalExpectedPdfFormat.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); } } @@ -219,37 +266,45 @@ class ImageToPdfTransformerTest try (PDDocument actualPdfDocument = PDDocument.load(targetFile)) { + BufferedImage actualImage = ImageIO.read(sourceFile); assertNotNull(actualPdfDocument); - assertEquals(PDRectangle.A4.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); - assertEquals(PDRectangle.A4.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); + assertEquals(actualImage.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); + assertEquals(actualImage.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); + } + } + + @Test + void testTransformImageToPDF_withInvalidPdfOrientationAndUsingDefaultOne() throws Exception + { + TransformOptions transformOptions = TransformOptions.of(null, "INVALID"); + + // when + transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager); + + try (PDDocument actualPdfDocument = PDDocument.load(targetFile)) + { + BufferedImage actualImage = ImageIO.read(sourceFile); + assertNotNull(actualPdfDocument); + assertEquals(actualImage.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); + assertEquals(actualImage.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); } } //----------------------------------------------- Helper methods and classes ----------------------------------------------- - private static PDRectangle resolveExpectedPdfFormat(String pdfFormat) + private static BiFunction unchangedRectangle() { - switch (pdfFormat.toUpperCase()) { - case "LETTER": - return PDRectangle.LETTER; - case "LEGAL": - return PDRectangle.LEGAL; - case "A0": - return PDRectangle.A0; - case "A1": - return PDRectangle.A1; - case "A2": - return PDRectangle.A2; - case "A3": - return PDRectangle.A3; - case "A5": - return PDRectangle.A5; - case "A6": - return PDRectangle.A6; - case "A4": - default: - return PDRectangle.A4; + return rectangleRotatedIf(null); + } + + private static BiFunction rectangleRotatedIf(BiPredicate predicate) + { + if (predicate == null) + { + return PDRectangle::new; } + + return (width, height) -> predicate.test(width, height)? new PDRectangle(height, width) : new PDRectangle(width, height); } private static File loadFile(String fileName) @@ -302,12 +357,14 @@ class ImageToPdfTransformerTest Integer startPage; Integer endPage; String pdfFormat; + String pdfOrientation; - private TransformOptions(Integer startPage, Integer endPage, String pdfFormat) + private TransformOptions(Integer startPage, Integer endPage, String pdfFormat, String pdfOrientation) { this.startPage = startPage; this.endPage = endPage; this.pdfFormat = pdfFormat; + this.pdfOrientation = pdfOrientation; } public Map toMap() @@ -325,17 +382,26 @@ class ImageToPdfTransformerTest { transformOptions.put(PDF_FORMAT, pdfFormat); } + if (pdfOrientation != null) + { + transformOptions.put(PDF_ORIENTATION, pdfOrientation); + } return transformOptions; } public static TransformOptions of(Integer startPage, Integer endPage) { - return new TransformOptions(startPage, endPage, null); + return new TransformOptions(startPage, endPage, null, null); } public static TransformOptions of(String pdfFormat) { - return new TransformOptions(null, null, pdfFormat); + return new TransformOptions(null, null, pdfFormat, null); + } + + public static TransformOptions of(String pdfFormat, String pdfOrientation) + { + return new TransformOptions(null, null, pdfFormat, pdfOrientation); } public static TransformOptions none() @@ -349,4 +415,17 @@ class ImageToPdfTransformerTest return "TransformOption{" + "startPage=" + startPage + ", endPage=" + endPage + '}'; } } + + static { + try + { + BufferedImage image = ImageIO.read(sourceFile); + sourceFileWidth = image.getWidth(); + sourceFileHeight = image.getHeight(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } } \ No newline at end of file diff --git a/engines/misc/src/test/java/org/alfresco/transform/misc/util/ArgumentsCartesianProduct.java b/engines/misc/src/test/java/org/alfresco/transform/misc/util/ArgumentsCartesianProduct.java index d08387a3..f3097ccf 100644 --- a/engines/misc/src/test/java/org/alfresco/transform/misc/util/ArgumentsCartesianProduct.java +++ b/engines/misc/src/test/java/org/alfresco/transform/misc/util/ArgumentsCartesianProduct.java @@ -27,6 +27,7 @@ package org.alfresco.transform.misc.util; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -42,7 +43,7 @@ import org.junit.jupiter.params.provider.Arguments; public class ArgumentsCartesianProduct { /** - * Creates cartesian product of fixed argument and a stream of arguments. + * Creates arguments cartesian product of fixed object and a stream of objects. * Example: a ✕ {x,y,z} = {a,x}, {a,y}, {a,z} */ public static Stream of(final Object fixedFirstArgument, final Stream secondArguments) @@ -51,7 +52,7 @@ public class ArgumentsCartesianProduct } /** - * Creates cartesian product of a stream of arguments and fixed arguments. + * Creates arguments cartesian product of a stream of objects and fixed object. * Example: {a,b,c} ✕ y ✕ z = {a,y,z}, {b,y,z}, {c,y,z} */ public static Stream of(final Stream firstArguments, final Object... otherFixedArguments) @@ -60,7 +61,7 @@ public class ArgumentsCartesianProduct } /** - * Creates cartesian product of two streams of arguments. + * Creates arguments cartesian product of two streams of objects. * Example: {a,b} ✕ {y,z} = {a,y}, {a,z}, {b,y}, {b,z} */ public static Stream of(final Stream firstArguments, final Stream secondArguments) @@ -69,7 +70,7 @@ public class ArgumentsCartesianProduct } /** - * Creates cartesian product of multiple streams of arguments. + * Creates arguments cartesian product of multiple streams of objects. * Example: {a,b} ✕ {k,l,m} ✕ ... ✕ {y,z} = {a,k,...,y}, {a,k,...,z}, {a,l,...,y}, ..., {b,m,...,z} */ public static Stream of(final Stream... argumentsStreams) @@ -77,6 +78,21 @@ public class ArgumentsCartesianProduct return cartesianProductOf(argumentsStreams).map(arguments -> Arguments.of(arguments.toArray())); } + /** + * Creates arguments cartesian product of multiple streams of arguments. + * Example: {a,b} ✕ {k,l,m} ✕ ... ✕ {y,z} = {a,k,...,y}, {a,k,...,z}, {a,l,...,y}, ..., {b,m,...,z} + */ + @SafeVarargs + @SuppressWarnings("unchecked") + public static Stream ofArguments(final Stream... argumentsStreams) + { + return cartesianProductOf(argumentsStreams) + .map(argumentsStream -> (Stream) argumentsStream) + .map(argumentsStream -> Arguments.of(argumentsStream + .flatMap(arguments -> Arrays.stream(arguments.get())) + .toArray())); + } + private static Stream> cartesianProductOf(final Stream... streams) { if (streams == null) diff --git a/model/src/main/java/org/alfresco/transform/common/RequestParamMap.java b/model/src/main/java/org/alfresco/transform/common/RequestParamMap.java index 31195e1a..1647accc 100644 --- a/model/src/main/java/org/alfresco/transform/common/RequestParamMap.java +++ b/model/src/main/java/org/alfresco/transform/common/RequestParamMap.java @@ -65,6 +65,7 @@ public interface RequestParamMap String NOT_EXTRACT_BOOKMARKS_TEXT = "notExtractBookmarksText"; String PAGE_LIMIT = "pageLimit"; String PDF_FORMAT = "pdfFormat"; + String PDF_ORIENTATION = "pdfOrientation"; // Parameters interpreted by the TransformController String DIRECT_ACCESS_URL = "directAccessUrl";