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 e8960b64..e4582a45 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 @@ -63,13 +63,15 @@ public class AIOTikaTest extends TikaTest "notExtractBookmarksText", "page", "pageLimit", + "pdfFormat", "resizeHeight", "resizePercentage", "resizeWidth", "startPage", "targetEncoding", "thumbnail", - "width"), + "width" + ), getOptionNames(controller.transformConfig(0).getBody().getTransformOptions())); } } \ No newline at end of file diff --git a/engines/imagemagick/src/main/resources/imagemagick_engine_config.json b/engines/imagemagick/src/main/resources/imagemagick_engine_config.json index add6f6a7..f7cdcc74 100644 --- a/engines/imagemagick/src/main/resources/imagemagick_engine_config.json +++ b/engines/imagemagick/src/main/resources/imagemagick_engine_config.json @@ -932,7 +932,7 @@ {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-raw-minolta" }, {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-raw-nikon" }, {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-raw-olympus" }, - {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-portable-bitmap" }, + {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-portable-bitmap" }, {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-raw-pentax" }, {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-portable-graymap" }, {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-portable-anymap" }, @@ -950,7 +950,6 @@ {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-xbitmap" }, {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-xpixmap" }, {"sourceMediaType": "image/tiff", "targetMediaType": "image/x-xwindowdump" }, - {"sourceMediaType": "image/tiff", "targetMediaType": "application/pdf" }, {"sourceMediaType": "image/x-raw-sigma", "targetMediaType": "image/x-raw-hasselblad" }, {"sourceMediaType": "image/x-raw-sigma", "targetMediaType": "image/x-raw-sony" }, 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 new file mode 100644 index 00000000..e8c611e4 --- /dev/null +++ b/engines/misc/src/main/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformer.java @@ -0,0 +1,218 @@ +/* + * #%L + * Alfresco Transform Core + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco 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 Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +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.START_PAGE; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.Comparator; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import org.alfresco.transform.base.TransformManager; +import org.alfresco.transform.base.util.CustomTransformerFileAdaptor; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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. + */ +@Component +public class ImageToPdfTransformer implements CustomTransformerFileAdaptor +{ + private static final Logger log = LoggerFactory.getLogger(ImageToPdfTransformer.class); + + private static final String NEGATIVE_START_PAGE_ERROR_MESSAGE = "Start page number cannot be a negative number."; + private static final String NEGATIVE_END_PAGE_ERROR_MESSAGE = "End page number cannot be a negative number."; + 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; + + @Override + public String getTransformerName() + { + return "imageToPdf"; + } + + @Override + public void transform( + String sourceMimetype, String targetMimetype, Map transformOptions, + File imageFile, File pdfFile, TransformManager transformManager + ) throws Exception { + try ( + ImageInputStream imageInputStream = ImageIO.createImageInputStream(imageFile); + PDDocument pdfDocument = new PDDocument() + ) { + 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); + verifyOptions(startPage, endPage); + + final ImageReader imageReader = findImageReader(imageInputStream, imageFile.getName(), sourceMimetype); + for (int i = 0; i < imageReader.getNumImages(true); i++) + { + if (startPage != null && i < startPage) + { + continue; + } + if (endPage != null && i > endPage) + { + break; + } + + scaleAndDrawImage(pdfDocument, imageReader.read(i), pdfFormat); + } + + pdfDocument.save(pdfFile); + } + } + + private ImageReader findImageReader(final ImageInputStream imageInputStream, final String imageName, final String mimetype) throws IOException + { + final Iterator imageReaders = ImageIO.getImageReaders(imageInputStream); + if (imageReaders == null || !imageReaders.hasNext()) + { + throw new IOException(String.format(INVALID_IMAGE_ERROR_MESSAGE, imageName, mimetype)); + } + final ImageReader imageReader = imageReaders.next(); + imageReader.setInput(imageInputStream); + + return imageReader; + } + + private void scaleAndDrawImage(final PDDocument pdfDocument, final BufferedImage bufferedImage, final String pdfFormat) throws IOException + { + final PDPage pdfPage = new PDPage(resolvePdfFormat(pdfFormat)); + pdfDocument.addPage(pdfPage); + final PDImageXObject image = LosslessFactory.createFromImage(pdfDocument, bufferedImage); + try (PDPageContentStream pdfPageContent = new PDPageContentStream(pdfDocument, pdfPage)) + { + final PDRectangle pageSize = pdfPage.getMediaBox(); + final float widthRatio = image.getWidth() > 0 ? pageSize.getWidth() / image.getWidth() : 0; + final float heightRatio = image.getHeight() > 0 ? pageSize.getHeight() / image.getHeight() : 0; + final float ratio = Stream.of(widthRatio, heightRatio, 1f).min(Comparator.naturalOrder()).get(); + // find image bottom + final float y = pageSize.getHeight() - image.getHeight() * ratio; + // drawing starts from bottom left corner + pdfPageContent.drawImage(image, 0, y, image.getWidth() * ratio, image.getHeight() * ratio); + } + } + + private PDRectangle resolvePdfFormat(final String pdfFormat) + { + switch (pdfFormat.toUpperCase()) { + case "A4": + return DEFAULT_PDF_FORMAT; + case "LETTER": + return PDRectangle.LETTER; + 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 "LEGAL": + return PDRectangle.LEGAL; + default: + log.info("PDF format: '{}' not supported. Using default: '{}'", pdfFormat, DEFAULT_PDF_FORMAT_STRING); + return DEFAULT_PDF_FORMAT; + } + } + + private static Optional parseOptionIfPresent(final Map transformOptions, final String parameter, final Class targetType) + { + if (transformOptions.containsKey(parameter)) + { + final String option = transformOptions.get(parameter); + if (targetType == Integer.class) + { + try + { + return Optional.of(targetType.cast(Integer.parseInt(option))); + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException(String.format(INVALID_OPTION_ERROR_MESSAGE, parameter, option)); + } + } + else + { + return Optional.of(targetType.cast(option)); + } + } + + return Optional.empty(); + } + + private static void verifyOptions(final Integer startPage, final Integer endPage) + { + if (startPage != null && startPage < 0) + { + throw new IllegalArgumentException(NEGATIVE_START_PAGE_ERROR_MESSAGE); + } + + if (endPage != null && endPage < 0) + { + throw new IllegalArgumentException(NEGATIVE_END_PAGE_ERROR_MESSAGE); + } + + if (startPage != null && endPage != null && startPage > endPage) + { + throw new IllegalArgumentException(START_PAGE_GREATER_THAN_END_PAGE_ERROR_MESSAGE); + } + } +} diff --git a/engines/misc/src/main/resources/misc_engine_config.json b/engines/misc/src/main/resources/misc_engine_config.json index 41700f84..62c466c4 100644 --- a/engines/misc/src/main/resources/misc_engine_config.json +++ b/engines/misc/src/main/resources/misc_engine_config.json @@ -8,6 +8,11 @@ ], "metadataOptions": [ {"value": {"name": "extractMapping"}} + ], + "imageToPdfOptions": [ + {"value": {"name": "startPage"}}, + {"value": {"name": "endPage"}}, + {"value": {"name": "pdfFormat"}} ] }, "transformers": [ @@ -91,6 +96,15 @@ "transformOptions": [ "metadataOptions" ] + }, + { + "transformerName": "imageToPdf", + "supportedSourceAndTargetList": [ + {"sourceMediaType": "image/tiff", "targetMediaType": "application/pdf"} + ], + "transformOptions": [ + "imageToPdfOptions" + ] } ] } \ No newline at end of file diff --git a/engines/misc/src/test/java/org/alfresco/transform/misc/MiscTransformsIT.java b/engines/misc/src/test/java/org/alfresco/transform/misc/MiscTransformsIT.java index b6984c6c..d8625041 100644 --- a/engines/misc/src/test/java/org/alfresco/transform/misc/MiscTransformsIT.java +++ b/engines/misc/src/test/java/org/alfresco/transform/misc/MiscTransformsIT.java @@ -26,21 +26,12 @@ */ package org.alfresco.transform.misc; -import org.alfresco.transform.base.clients.FileInfo; -import org.alfresco.transform.base.clients.SourceTarget; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.core.io.Resource; -import org.springframework.http.ResponseEntity; - -import java.util.Map; -import java.util.stream.Stream; - import static java.text.MessageFormat.format; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; -import static org.alfresco.transform.base.clients.HttpClient.sendTRequest; + import static org.alfresco.transform.base.clients.FileInfo.testFile; +import static org.alfresco.transform.base.clients.HttpClient.sendTRequest; import static org.alfresco.transform.common.Mimetype.MIMETYPE_DITA; import static org.alfresco.transform.common.Mimetype.MIMETYPE_EXCEL; import static org.alfresco.transform.common.Mimetype.MIMETYPE_HTML; @@ -67,9 +58,22 @@ import static org.alfresco.transform.common.Mimetype.MIMETYPE_TEXT_PLAIN; import static org.alfresco.transform.common.Mimetype.MIMETYPE_WORD; import static org.alfresco.transform.common.Mimetype.MIMETYPE_XML; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; import static org.springframework.http.HttpStatus.OK; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +import org.alfresco.transform.base.clients.FileInfo; +import org.alfresco.transform.base.clients.SourceTarget; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.core.io.Resource; +import org.springframework.http.ResponseEntity; + /** * @author Cezar Leahu */ @@ -78,10 +82,10 @@ public class MiscTransformsIT private static final String ENGINE_URL = "http://localhost:8090"; private static final Map TEST_FILES = Stream.of( - testFile(MIMETYPE_IMAGE_GIF, "gif", "quick.gif"), - testFile(MIMETYPE_IMAGE_JPEG, "jpg", "quick.jpg"), - testFile(MIMETYPE_IMAGE_PNG, "png", "quick.png"), - testFile(MIMETYPE_IMAGE_TIFF, "tiff", "quick.tiff"), + testFile(MIMETYPE_IMAGE_GIF, "gif", "sample.gif"), + testFile(MIMETYPE_IMAGE_JPEG, "jpg", "sample.jpg"), + testFile(MIMETYPE_IMAGE_PNG, "png", "sample.png"), + testFile(MIMETYPE_IMAGE_TIFF, "tiff", "sample.tiff"), testFile(MIMETYPE_WORD, "doc", "quick.doc"), testFile(MIMETYPE_OPENXML_WORDPROCESSING, "docx", "quick.docx"), testFile(MIMETYPE_EXCEL, "xls", "quick.xls"), @@ -146,6 +150,8 @@ public class MiscTransformsIT SourceTarget.of("application/dita+xml", "application/pdf"), SourceTarget.of("text/xml", "application/pdf"), + SourceTarget.of(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF), + SourceTarget.of("message/rfc822", "text/plain") ); } @@ -164,9 +170,17 @@ public class MiscTransformsIT try { + // when final ResponseEntity response = sendTRequest(ENGINE_URL, sourceFile, sourceMimetype, targetMimetype, targetExtension); + assertEquals(OK, response.getStatusCode(), descriptor); + if (MIMETYPE_PDF.equals(targetMimetype)) + { + // verify if PDF isn't corrupted + final PDDocument pdfFile = PDDocument.load(Objects.requireNonNull(response.getBody()).getInputStream()); + assertNotNull(pdfFile); + } } catch (Exception e) { 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 new file mode 100644 index 00000000..cd519e49 --- /dev/null +++ b/engines/misc/src/test/java/org/alfresco/transform/misc/transformers/ImageToPdfTransformerTest.java @@ -0,0 +1,352 @@ +/* + * #%L + * Alfresco Transform Core + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco 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 Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.transform.misc.transformers; + +import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_GIF; +import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG; +import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_PNG; +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.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 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.Function; +import java.util.stream.Stream; + +import org.alfresco.transform.base.TransformManager; +import org.alfresco.transform.misc.util.ArgumentsCartesianProduct; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class ImageToPdfTransformerTest +{ + private static final File sourceFile = loadFile("sample.gif"); + + @Mock + private TransformManager transformManager; + + @InjectMocks + private ImageToPdfTransformer transformer; + + private File targetFile = null; + + @BeforeEach + void setUp() throws IOException + { + MockitoAnnotations.openMocks(this); + + targetFile = File.createTempFile("temp_target", ".pdf"); + } + + static Stream imageFiles() + { + return Stream.of( + ImageFile.of("sample.jpg", MIMETYPE_IMAGE_JPEG), + ImageFile.of("sample.gif", MIMETYPE_IMAGE_GIF), + ImageFile.of("sample.png", MIMETYPE_IMAGE_PNG) + ); + } + + static Stream defaultTransformOptions() + { + return Stream.of( + TransformOptions.none(), + TransformOptions.of(0, null), + TransformOptions.of(0, 0) + ); + } + + static Stream tiffTransformOptions() + { + return Stream.of( + TransformOptions.of(0, 0), // (startPage, endPage) + TransformOptions.of(0, 1), + TransformOptions.of(1, 1), + 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.none() // expected all pages in target file + ); + } + + static Stream transformSourcesAndOptions() + { + return Stream.of( + ArgumentsCartesianProduct.of(imageFiles(), defaultTransformOptions()), + ArgumentsCartesianProduct.of(ImageFile.of("sample.tiff", MIMETYPE_IMAGE_TIFF, 6), tiffTransformOptions()) + ).flatMap(Function.identity()); + } + + @ParameterizedTest + @MethodSource("transformSourcesAndOptions") + void testTransformImageToPdf(ImageFile imageFile, TransformOptions transformOptions) throws Exception + { + File sourceFile = loadFile(imageFile.fileName); + + // when + transformer.transform(imageFile.mimetype, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager); + + then(transformManager).shouldHaveNoInteractions(); + try (PDDocument actualPdfDocument = PDDocument.load(targetFile)) + { + int expectedNumberOfPages = calculateExpectedNumberOfPages(transformOptions, imageFile.firstPage(), imageFile.lastPage()); + assertNotNull(actualPdfDocument); + assertEquals(expectedNumberOfPages, actualPdfDocument.getNumberOfPages()); + } + } + + private static int calculateExpectedNumberOfPages(TransformOptions transformOptions, int firstPage, int lastPage) + { + int startPage = Optional.ofNullable(transformOptions.startPage).orElse(firstPage); + int endPage = Math.min(Optional.ofNullable(transformOptions.endPage).orElse(lastPage), lastPage); + return endPage - startPage + 1; + } + + static Stream improperTransformOptions() + { + return Stream.of( + TransformOptions.of(1, 0), + TransformOptions.of(-1, 0), + TransformOptions.of(0, -1) + ); + } + + @ParameterizedTest + @MethodSource("improperTransformOptions") + void testTransformTiffToPdf_withImproperOptions(TransformOptions transformOptions) + { + // when + assertThrows(IllegalArgumentException.class, () -> + transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager)); + } + + @Test + void testTransformTiffToPdf_withInvalidStartPageOption() + { + Map transformOptions = TransformOptions.none().toMap(); + transformOptions.put(START_PAGE, "a"); + + // when + assertThrows(IllegalArgumentException.class, () -> + transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions, sourceFile, targetFile, transformManager)); + } + + @Test + void testTransformTiffToPdf_withInvalidEndPageOption() + { + Map transformOptions = TransformOptions.none().toMap(); + transformOptions.put(END_PAGE, "z"); + + // when + assertThrows(IllegalArgumentException.class, () -> + transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions, sourceFile, targetFile, transformManager)); + } + + static Stream validPdfFormats() + { + return Stream.of("A0", "a0", "A1", "A2", "A3", "A4", "A5", "A6", "a6", "LETTER", "letter", "LEGAL", "legal"); + } + + @ParameterizedTest + @MethodSource("validPdfFormats") + void testTransformImageToPDF_withVariousPdfFormats(String pdfFormat) throws Exception + { + TransformOptions transformOptions = TransformOptions.of(pdfFormat); + + // when + transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager); + + try (PDDocument actualPdfDocument = PDDocument.load(targetFile)) + { + PDRectangle expectedPdfFormat = resolveExpectedPdfFormat(pdfFormat); + assertNotNull(actualPdfDocument); + assertEquals(expectedPdfFormat.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); + assertEquals(expectedPdfFormat.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); + } + } + + @Test + void testTransformImageToPDF_withInvalidPdfFormatAndUsingDefaultOne() throws Exception + { + TransformOptions transformOptions = TransformOptions.of("INVALID"); + + // when + transformer.transform(MIMETYPE_IMAGE_TIFF, MIMETYPE_PDF, transformOptions.toMap(), sourceFile, targetFile, transformManager); + + try (PDDocument actualPdfDocument = PDDocument.load(targetFile)) + { + assertNotNull(actualPdfDocument); + assertEquals(PDRectangle.A4.getWidth(), actualPdfDocument.getPage(0).getMediaBox().getWidth()); + assertEquals(PDRectangle.A4.getHeight(), actualPdfDocument.getPage(0).getMediaBox().getHeight()); + } + } + + //----------------------------------------------- Helper methods and classes ----------------------------------------------- + + private static PDRectangle resolveExpectedPdfFormat(String pdfFormat) + { + 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; + } + } + + private static File loadFile(String fileName) + { + return new File(Objects.requireNonNull(ImageToPdfTransformerTest.class.getClassLoader().getResource(fileName)).getFile()); + } + + private static class ImageFile + { + String fileName; + String mimetype; + int numberOfPages; + + private ImageFile(String fileName, String mimetype, int numberOfPages) + { + this.fileName = fileName; + this.mimetype = mimetype; + this.numberOfPages = numberOfPages; + } + + public static ImageFile of(String fileName, String mimetype, int numberOfPages) + { + return new ImageFile(fileName, mimetype, numberOfPages); + } + + public static ImageFile of(String fileName, String mimetype) + { + return of(fileName, mimetype, 1); + } + + public int firstPage() + { + return 0; + } + + public int lastPage() + { + return numberOfPages - 1; + } + + @Override + public String toString() + { + return "ImageFile{" + "fileName='" + fileName + '\'' + ", mimetype='" + mimetype + '\'' + '}'; + } + } + + private static class TransformOptions + { + Integer startPage; + Integer endPage; + String pdfFormat; + + private TransformOptions(Integer startPage, Integer endPage, String pdfFormat) + { + this.startPage = startPage; + this.endPage = endPage; + this.pdfFormat = pdfFormat; + } + + public Map toMap() + { + final Map transformOptions = new HashMap<>(); + if (startPage != null) + { + transformOptions.put(START_PAGE, startPage.toString()); + } + if (endPage != null) + { + transformOptions.put(END_PAGE, endPage.toString()); + } + if (pdfFormat != null) + { + transformOptions.put(PDF_FORMAT, pdfFormat); + } + return transformOptions; + } + + public static TransformOptions of(Integer startPage, Integer endPage) + { + return new TransformOptions(startPage, endPage, null); + } + + public static TransformOptions of(String pdfFormat) + { + return new TransformOptions(null, null, pdfFormat); + } + + public static TransformOptions none() + { + return TransformOptions.of(null); + } + + @Override + public String toString() + { + return "TransformOption{" + "startPage=" + startPage + ", endPage=" + endPage + '}'; + } + } +} \ 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 new file mode 100644 index 00000000..d08387a3 --- /dev/null +++ b/engines/misc/src/test/java/org/alfresco/transform/misc/util/ArgumentsCartesianProduct.java @@ -0,0 +1,101 @@ +/* + * #%L + * Alfresco Transform Core + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco 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 Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.transform.misc.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.params.provider.Arguments; + +/** + * Creates a cartesian product of arguments provided either in streams or as an objects. Result is a {@link Stream} of JUnit's {@link Arguments}. + */ +public class ArgumentsCartesianProduct +{ + /** + * Creates cartesian product of fixed argument and a stream of arguments. + * Example: a ✕ {x,y,z} = {a,x}, {a,y}, {a,z} + */ + public static Stream of(final Object fixedFirstArgument, final Stream secondArguments) + { + return secondArguments.map(secondArgument -> Arguments.of(fixedFirstArgument, secondArgument)); + } + + /** + * Creates cartesian product of a stream of arguments and fixed arguments. + * 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) + { + return firstArguments.map(firstArgument -> Arguments.of(firstArgument, otherFixedArguments)); + } + + /** + * Creates cartesian product of two streams of arguments. + * Example: {a,b} ✕ {y,z} = {a,y}, {a,z}, {b,y}, {b,z} + */ + public static Stream of(final Stream firstArguments, final Stream secondArguments) + { + return cartesianProductOf(firstArguments, secondArguments).map(arguments -> Arguments.of(arguments.toArray())); + } + + /** + * Creates 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} + */ + public static Stream of(final Stream... argumentsStreams) + { + return cartesianProductOf(argumentsStreams).map(arguments -> Arguments.of(arguments.toArray())); + } + + private static Stream> cartesianProductOf(final Stream... streams) + { + if (streams == null) + { + return Stream.empty(); + } + + return Stream.of(streams) + .filter(Objects::nonNull) + .map(stream -> stream.map(Collections::singletonList)) + .reduce((result, nextElements) -> { + final List> nextElementsCopy = nextElements.collect(Collectors.toList()); + return result.flatMap(resultPortion -> nextElementsCopy.stream().map(nextElementsPortion -> { + final List extendedResultPortion = new ArrayList<>(); + extendedResultPortion.addAll(resultPortion); + extendedResultPortion.addAll(nextElementsPortion); + return extendedResultPortion; + })); + }).orElse(Stream.empty()) + .map(Collection::stream); + } +} diff --git a/engines/misc/src/test/resources/sample.gif b/engines/misc/src/test/resources/sample.gif new file mode 100644 index 00000000..59f4989e Binary files /dev/null and b/engines/misc/src/test/resources/sample.gif differ diff --git a/engines/misc/src/test/resources/sample.jpg b/engines/misc/src/test/resources/sample.jpg new file mode 100644 index 00000000..1c1e377a Binary files /dev/null and b/engines/misc/src/test/resources/sample.jpg differ diff --git a/engines/misc/src/test/resources/sample.png b/engines/misc/src/test/resources/sample.png new file mode 100644 index 00000000..f1a155e6 Binary files /dev/null and b/engines/misc/src/test/resources/sample.png differ diff --git a/engines/misc/src/test/resources/sample.tiff b/engines/misc/src/test/resources/sample.tiff new file mode 100644 index 00000000..3061be2e Binary files /dev/null and b/engines/misc/src/test/resources/sample.tiff differ 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 0f47256f..31195e1a 100644 --- a/model/src/main/java/org/alfresco/transform/common/RequestParamMap.java +++ b/model/src/main/java/org/alfresco/transform/common/RequestParamMap.java @@ -64,6 +64,7 @@ public interface RequestParamMap String INCLUDE_CONTENTS = "includeContents"; String NOT_EXTRACT_BOOKMARKS_TEXT = "notExtractBookmarksText"; String PAGE_LIMIT = "pageLimit"; + String PDF_FORMAT = "pdfFormat"; // Parameters interpreted by the TransformController String DIRECT_ACCESS_URL = "directAccessUrl";